chore: changed and refactored providers (#36)

## 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: #36
Reviewed-by: Marcel_Henselin <marcel.henselin@stackit.cloud>
Co-authored-by: Andre Harms <andre.harms@stackit.cloud>
Co-committed-by: Andre Harms <andre.harms@stackit.cloud>
This commit is contained in:
Andre_Harms 2026-02-10 08:10:02 +00:00 committed by Marcel_Henselin
parent b1b359f436
commit de019908d2
Signed by: tf-provider.git.onstackit.cloud
GPG key ID: 6D7E8A1ED8955A9C
70 changed files with 6250 additions and 2608 deletions

View file

@ -5,18 +5,17 @@ import (
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen"
postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
)
@ -30,6 +29,12 @@ func NewDatabaseDataSource() datasource.DataSource {
return &databaseDataSource{}
}
// dataSourceModel maps the data source schema data.
type dataSourceModel struct {
postgresflexalpha2.DatabaseModel
TerraformID types.String `tfsdk:"id"`
}
// databaseDataSource is the data source implementation.
type databaseDataSource struct {
client *postgresflexalpha.APIClient
@ -66,132 +71,46 @@ func (r *databaseDataSource) Configure(
}
// Schema defines the schema for the data source.
func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
descriptions := map[string]string{
"main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".",
"database_id": "Database ID.",
"instance_id": "ID of the Postgres Flex instance.",
"project_id": "STACKIT project ID to which the instance is associated.",
"name": "Database name.",
"owner": "Username of the database owner.",
"region": "The resource region. If not defined, the provider region is used.",
func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
s := postgresflexalpha2.DatabaseDataSourceSchema(ctx)
s.Attributes["id"] = schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," +
"`database_id`\\\".\",",
Computed: true,
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
},
"database_id": schema.Int64Attribute{
Description: descriptions["database_id"],
Optional: true,
Computed: true,
},
"instance_id": schema.StringAttribute{
Description: descriptions["instance_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"owner": schema.StringAttribute{
Description: descriptions["owner"],
Computed: true,
},
"region": schema.StringAttribute{
// the region cannot be found, so it has to be passed
Optional: true,
Description: descriptions["region"],
},
},
}
resp.Schema = s
}
// Read refreshes the Terraform state with the latest data.
// Read fetches the data for the data source.
func (r *databaseDataSource) Read(
ctx context.Context,
req datasource.ReadRequest,
resp *datasource.ReadResponse,
) { // nolint:gocritic // function signature required by Terraform
var model Model
var model dataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// validation for exactly one of database_id or name
isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown()
isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown()
if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) {
core.LogAndAddError(
ctx, &resp.Diagnostics,
"Invalid configuration", "Exactly one of 'database_id' or 'name' must be specified.",
)
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
databaseId := model.DatabaseId.ValueInt64()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = tflog.SetField(ctx, "region", region)
var databaseResp *postgresflexalpha.ListDatabase
var err error
if isIdSet {
databaseId := model.DatabaseId.ValueInt64()
ctx = tflog.SetField(ctx, "database_id", databaseId)
databaseResp, err = getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId)
} else {
databaseName := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", databaseName)
databaseResp, err = getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName)
databaseResp, err := r.getDatabaseByNameOrID(ctx, &model, projectId, region, instanceId, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading database",
fmt.Sprintf(
"Database with ID %q or instance with ID %q does not exist in project %q.",
databaseId,
instanceId,
projectId,
),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
)
handleReadError(ctx, &resp.Diagnostics, err, projectId, instanceId)
resp.State.RemoveResource(ctx)
return
}
@ -218,3 +137,60 @@ func (r *databaseDataSource) Read(
}
tflog.Info(ctx, "Postgres Flex database read")
}
// getDatabaseByNameOrID retrieves a single database by ensuring either a unique ID or name is provided.
func (r *databaseDataSource) getDatabaseByNameOrID(
ctx context.Context,
model *dataSourceModel,
projectId, region, instanceId string,
diags *diag.Diagnostics,
) (*postgresflexalpha.ListDatabase, error) {
isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown()
isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown()
if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) {
diags.AddError(
"Invalid configuration",
"Exactly one of 'id' or 'name' must be specified.",
)
return nil, nil
}
if isIdSet {
databaseId := model.DatabaseId.ValueInt64()
ctx = tflog.SetField(ctx, "database_id", databaseId)
return getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId)
}
databaseName := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", databaseName)
return getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName)
}
// handleReadError centralizes API error handling for the Read operation.
func handleReadError(ctx context.Context, diags *diag.Diagnostics, err error, projectId, instanceId string) {
utils.LogError(
ctx,
diags,
err,
"Reading database",
fmt.Sprintf(
"Could not retrieve database for instance %q in project %q.",
instanceId,
projectId,
),
map[int]string{
http.StatusBadRequest: fmt.Sprintf(
"Invalid request parameters for project %q and instance %q.",
projectId,
instanceId,
),
http.StatusNotFound: fmt.Sprintf(
"Database, instance %q, or project %q not found.",
instanceId,
projectId,
),
http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId),
},
)
}

View file

@ -0,0 +1,69 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package postgresflexalpha
import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
)
func DatabaseDataSourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"database_id": schema.Int64Attribute{
Required: true,
Description: "The ID of the database.",
MarkdownDescription: "The ID of the database.",
},
"tf_original_api_id": schema.Int64Attribute{
Computed: true,
Description: "The id of the database.",
MarkdownDescription: "The id of the database.",
},
"instance_id": schema.StringAttribute{
Required: true,
Description: "The ID of the instance.",
MarkdownDescription: "The ID of the instance.",
},
"name": schema.StringAttribute{
Computed: true,
Description: "The name of the database.",
MarkdownDescription: "The name of the database.",
},
"owner": schema.StringAttribute{
Computed: true,
Description: "The owner of the database.",
MarkdownDescription: "The owner of the database.",
},
"project_id": schema.StringAttribute{
Required: true,
Description: "The STACKIT project ID.",
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Required: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{
stringvalidator.OneOf(
"eu01",
),
},
},
},
}
}
type DatabaseModel struct {
DatabaseId types.Int64 `tfsdk:"database_id"`
Id types.Int64 `tfsdk:"tf_original_api_id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
}

View file

@ -3,6 +3,7 @@ package postgresflexalpha
import (
"context"
"fmt"
"strings"
postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
)
@ -79,3 +80,12 @@ func getDatabase(
return nil, fmt.Errorf("database not found for instance %s", instanceId)
}
// cleanString removes leading and trailing quotes which are sometimes returned by the API.
func cleanString(s *string) *string {
if s == nil {
return nil
}
res := strings.Trim(*s, "\"")
return &res
}

View file

@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
)
@ -12,8 +13,8 @@ type mockRequest struct {
executeFunc func() (*postgresflex.ListDatabasesResponse, error)
}
func (m *mockRequest) Page(_ int64) postgresflex.ApiListDatabasesRequestRequest { return m }
func (m *mockRequest) Size(_ int64) postgresflex.ApiListDatabasesRequestRequest { return m }
func (m *mockRequest) Page(_ int32) postgresflex.ApiListDatabasesRequestRequest { return m }
func (m *mockRequest) Size(_ int32) postgresflex.ApiListDatabasesRequestRequest { return m }
func (m *mockRequest) Sort(_ postgresflex.DatabaseSort) postgresflex.ApiListDatabasesRequestRequest {
return m
}
@ -176,21 +177,56 @@ func TestGetDatabase(t *testing.T) {
}
if (errDB != nil) != tt.wantErr {
t.Errorf("getDatabase() error = %v, wantErr %v", errDB, tt.wantErr)
t.Errorf("getDatabaseByNameOrID() error = %v, wantErr %v", errDB, tt.wantErr)
return
}
if !tt.wantErr && tt.wantDbName != "" && actual != nil {
if *actual.Name != tt.wantDbName {
t.Errorf("getDatabase() got name = %v, want %v", *actual.Name, tt.wantDbName)
t.Errorf("getDatabaseByNameOrID() got name = %v, want %v", *actual.Name, tt.wantDbName)
}
}
if !tt.wantErr && tt.wantDbId != 0 && actual != nil {
if *actual.Id != tt.wantDbId {
t.Errorf("getDatabase() got id = %v, want %v", *actual.Id, tt.wantDbId)
t.Errorf("getDatabaseByNameOrID() got id = %v, want %v", *actual.Id, tt.wantDbId)
}
}
},
)
}
}
func TestCleanString(t *testing.T) {
testcases := []struct {
name string
given *string
expected *string
}{
{
name: "should remove quotes",
given: utils.Ptr("\"quoted\""),
expected: utils.Ptr("quoted"),
},
{
name: "should handle nil",
given: nil,
expected: nil,
},
{
name: "should not change unquoted string",
given: utils.Ptr("unquoted"),
expected: utils.Ptr("unquoted"),
},
}
for _, tc := range testcases {
t.Run(
tc.name, func(t *testing.T) {
actual := cleanString(tc.given)
if diff := cmp.Diff(tc.expected, actual); diff != "" {
t.Errorf("string mismatch (-want +got):\n%s", diff)
}
},
)
}
}

View file

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

View file

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

View file

@ -0,0 +1,35 @@
fields:
- name: 'id'
modifiers:
- 'UseStateForUnknown'
- name: 'database_id'
modifiers:
- 'UseStateForUnknown'
validators:
- validate.NoSeparator
- validate.UUID
- name: 'instance_id'
validators:
- validate.NoSeparator
- validate.UUID
modifiers:
- 'RequiresReplace'
- 'UseStateForUnknown'
- name: 'project_id'
modifiers:
- 'RequiresReplace'
- 'UseStateForUnknown'
validators:
- validate.NoSeparator
- validate.UUID
- name: 'name'
validators:
- validate.NoSeparator
- name: 'region'
modifiers:
- 'RequiresReplace'

View file

@ -2,70 +2,73 @@ package postgresflexalpha
import (
"context"
_ "embed"
"errors"
"fmt"
"math"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"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"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/resources_gen"
postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils"
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
var (
// Ensure the implementation satisfies the expected interfaces.
_ resource.Resource = &databaseResource{}
_ resource.ResourceWithConfigure = &databaseResource{}
_ resource.ResourceWithImportState = &databaseResource{}
_ resource.ResourceWithModifyPlan = &databaseResource{}
)
_ resource.ResourceWithIdentity = &databaseResource{}
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
DatabaseId types.Int64 `tfsdk:"database_id"`
InstanceId types.String `tfsdk:"instance_id"`
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`
Region types.String `tfsdk:"region"`
}
// Define errors
errDatabaseNotFound = errors.New("database not found")
// Error message constants
extractErrorSummary = "extracting failed"
extractErrorMessage = "Extracting identity data: %v"
)
// NewDatabaseResource is a helper function to simplify the provider implementation.
func NewDatabaseResource() resource.Resource {
return &databaseResource{}
}
// resourceModel describes the resource data model.
type resourceModel = postgresflexalpha2.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 *postgresflexalpha.APIClient
providerData core.ProviderData
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
// ModifyPlan adjusts the plan to set the correct region.
func (r *databaseResource) ModifyPlan(
ctx context.Context,
req resource.ModifyPlanRequest,
resp *resource.ModifyPlanResponse,
) { // nolint:gocritic // function signature required by Terraform
var configModel Model
var configModel resourceModel
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
@ -75,7 +78,7 @@ func (r *databaseResource) ModifyPlan(
return
}
var planModel Model
var planModel resourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
@ -117,85 +120,46 @@ func (r *databaseResource) Configure(
tflog.Info(ctx, "Postgres Flex database client configured")
}
//go:embed planModifiers.yaml
var modifiersFileByte []byte
// Schema defines the schema for the resource.
func (r *databaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".",
"database_id": "Database ID.",
"instance_id": "ID of the Postgres Flex instance.",
"project_id": "STACKIT project ID to which the instance is associated.",
"name": "Database name.",
"owner": "Username of the database owner.",
"region": "The resource region. If not defined, the provider region is used.",
func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
s := postgresflexalpha2.DatabaseResourceSchema(ctx)
fields, err := utils.ReadModifiersConfig(modifiersFileByte)
if err != nil {
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
return
}
resp.Schema = schema.Schema{
Description: descriptions["main"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["id"],
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
err = utils.AddPlanModifiersToResourceSchema(fields, &s)
if err != nil {
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
return
}
resp.Schema = s
}
// IdentitySchema defines the schema for the resource's identity attributes.
func (r *databaseResource) IdentitySchema(
_ context.Context,
_ resource.IdentitySchemaRequest,
response *resource.IdentitySchemaResponse,
) {
response.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"project_id": identityschema.StringAttribute{
RequiredForImport: true,
},
"database_id": schema.Int64Attribute{
Description: descriptions["database_id"],
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
Validators: []validator.Int64{},
"region": identityschema.StringAttribute{
RequiredForImport: true,
},
"instance_id": schema.StringAttribute{
Description: descriptions["instance_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
"instance_id": identityschema.StringAttribute{
RequiredForImport: true,
},
"project_id": schema.StringAttribute{
Description: descriptions["project_id"],
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: descriptions["name"],
Required: true,
PlanModifiers: []planmodifier.String{},
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile("^[a-z]([a-z0-9]*)?$"),
"must start with a letter, must have lower case letters or numbers",
),
},
},
"owner": schema.StringAttribute{
Description: descriptions["owner"],
Required: true,
PlanModifiers: []planmodifier.String{},
},
"region": schema.StringAttribute{
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
Description: descriptions["region"],
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
"database_id": identityschema.Int64Attribute{
RequiredForImport: true,
},
},
}
@ -207,18 +171,26 @@ func (r *databaseResource) Create(
req resource.CreateRequest,
resp *resource.CreateResponse,
) { // nolint:gocritic // function signature required by Terraform
var model Model
var model resourceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := model.Region.ValueString()
instanceId := model.InstanceId.ValueString()
projectId := identityData.ProjectID.ValueString()
region := identityData.ProjectID.ValueString()
instanceId := identityData.InstanceID.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
@ -272,7 +244,7 @@ func (r *databaseResource) Create(
}
// Map response body to schema
err = mapFields(database, &model, region)
err = mapResourceFields(database, &model)
if err != nil {
core.LogAndAddError(
ctx,
@ -282,9 +254,21 @@ func (r *databaseResource) Create(
)
return
}
// Set data returned by API in identity
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
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
@ -297,23 +281,36 @@ func (r *databaseResource) Read(
req resource.ReadRequest,
resp *resource.ReadResponse,
) { // nolint:gocritic // function signature required by Terraform
var model Model
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
databaseId := model.DatabaseId.ValueInt64()
region := r.providerData.GetRegionWithOverride(model.Region)
projectId, instanceId, region, databaseId, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_id", databaseId)
databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId)
if err != nil {
@ -329,7 +326,7 @@ func (r *databaseResource) Read(
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(databaseResp, &model, region)
err = mapResourceFields(databaseResp, &model)
if err != nil {
core.LogAndAddError(
ctx,
@ -355,32 +352,45 @@ func (r *databaseResource) Update(
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var model Model
var model resourceModel
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
databaseId64 := model.DatabaseId.ValueInt64()
projectId, instanceId, region, databaseId64, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
if databaseId64 > math.MaxInt32 {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)")
return
}
databaseId := int32(databaseId64)
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_id", databaseId)
// Retrieve values from state
var stateModel Model
var stateModel resourceModel
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -420,7 +430,7 @@ func (r *databaseResource) Update(
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFieldsUpdatePartially(res, &model, region)
err = mapResourceFields(res.Database, &model)
if err != nil {
core.LogAndAddError(
ctx,
@ -445,29 +455,41 @@ func (r *databaseResource) Delete(
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) { // nolint:gocritic // function signature required by Terraform
var model Model
var model resourceModel
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read identity data
var identityData DatabaseResourceIdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
databaseId64 := model.DatabaseId.ValueInt64()
projectId, instanceId, region, databaseId64, errExt := r.extractIdentityData(model, identityData)
if errExt != nil {
core.LogAndAddError(
ctx,
&resp.Diagnostics,
extractErrorSummary,
fmt.Sprintf(extractErrorMessage, errExt),
)
}
if databaseId64 > math.MaxInt32 {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)")
return
}
databaseId := int32(databaseId64)
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "database_id", databaseId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "database_id", databaseId)
// Delete existing record set
err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId)
@ -481,95 +503,118 @@ func (r *databaseResource) Delete(
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,zone_id,record_set_id
// The expected import identifier format is: [project_id],[region],[instance_id],[database_id]
func (r *databaseResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
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,
),
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 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(
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")
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"), idParts[3])...)
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.",
)
// 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
}
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)...)
tflog.Info(ctx, "Postgres Flex database state imported")
}
func mapFields(resp *postgresflexalpha.ListDatabase, model *Model, region string) error {
if resp == nil {
return fmt.Errorf("response is nil")
}
if resp.Id == nil || *resp.Id == 0 {
return fmt.Errorf("id not present")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var databaseId int64
if model.DatabaseId.ValueInt64() != 0 {
// 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 resp.Id != nil {
databaseId = *resp.Id
} else {
return fmt.Errorf("database id not present")
}
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(databaseId, 10),
)
model.DatabaseId = types.Int64Value(databaseId)
model.Name = types.StringPointerValue(resp.Name)
model.Region = types.StringValue(region)
model.Owner = types.StringPointerValue(cleanString(resp.Owner))
return nil
}
func mapFieldsUpdatePartially(
res *postgresflexalpha.UpdateDatabasePartiallyResponse,
model *Model,
region string,
) error {
if res == nil {
return fmt.Errorf("response is nil")
}
return mapFields(res.Database, model, region)
}
func cleanString(s *string) *string {
if s == nil {
return nil
}
res := strings.Trim(*s, "\"")
return &res
}
func toCreatePayload(model *Model) (*postgresflexalpha.CreateDatabaseRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
if identity.DatabaseID.IsNull() || identity.DatabaseID.IsUnknown() {
return "", "", "", 0, fmt.Errorf("database_id not found in config")
}
databaseId = identity.DatabaseID.ValueInt64()
}
return &postgresflexalpha.CreateDatabaseRequestPayload{
Name: model.Name.ValueStringPointer(),
Owner: model.Owner.ValueStringPointer(),
}, nil
}
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()
}
var errDatabaseNotFound = errors.New("database not found")
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

@ -1,232 +0,0 @@
package postgresflexalpha
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha"
)
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
input *postgresflex.ListDatabase
region string
expected Model
isValid bool
}{
{
"default_values",
&postgresflex.ListDatabase{
Id: utils.Ptr(int64(1)),
},
testRegion,
Model{
Id: types.StringValue("pid,region,iid,1"),
DatabaseId: types.Int64Value(int64(1)),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Name: types.StringNull(),
Owner: types.StringNull(),
Region: types.StringValue(testRegion),
},
true,
},
{
"simple_values",
&postgresflex.ListDatabase{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr("dbname"),
Owner: utils.Ptr("username"),
},
testRegion,
Model{
Id: types.StringValue("pid,region,iid,1"),
DatabaseId: types.Int64Value(int64(1)),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Name: types.StringValue("dbname"),
Owner: types.StringValue("username"),
Region: types.StringValue(testRegion),
},
true,
},
{
"null_fields_and_int_conversions",
&postgresflex.ListDatabase{
Id: utils.Ptr(int64(1)),
Name: utils.Ptr(""),
Owner: utils.Ptr(""),
},
testRegion,
Model{
Id: types.StringValue("pid,region,iid,1"),
DatabaseId: types.Int64Value(int64(1)),
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Name: types.StringValue(""),
Owner: types.StringValue(""),
Region: types.StringValue(testRegion),
},
true,
},
{
"nil_response",
nil,
testRegion,
Model{},
false,
},
{
"empty_response",
&postgresflex.ListDatabase{},
testRegion,
Model{},
false,
},
{
"no_resource_id",
&postgresflex.ListDatabase{
Id: utils.Ptr(int64(0)),
Name: utils.Ptr("dbname"),
Owner: utils.Ptr("username"),
},
testRegion,
Model{},
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
state := &Model{
ProjectId: tt.expected.ProjectId,
InstanceId: tt.expected.InstanceId,
}
err := mapFields(tt.input, state, tt.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(state, &tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *postgresflex.CreateDatabaseRequestPayload
isValid bool
}{
{
"default_values",
&Model{
Name: types.StringValue("dbname"),
Owner: types.StringValue("username"),
},
&postgresflex.CreateDatabaseRequestPayload{
Name: utils.Ptr("dbname"),
Owner: utils.Ptr("username"),
},
true,
},
{
"null_fields",
&Model{
Name: types.StringNull(),
Owner: types.StringNull(),
},
&postgresflex.CreateDatabaseRequestPayload{
Name: nil,
Owner: nil,
},
true,
},
{
"nil_model",
nil,
nil,
false,
},
}
for _, tt := range tests {
t.Run(
tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
},
)
}
}
func Test_cleanString(t *testing.T) {
type args struct {
s *string
}
tests := []struct {
name string
args args
want *string
}{
{
name: "simple_value",
args: args{
s: utils.Ptr("mytest"),
},
want: utils.Ptr("mytest"),
},
{
name: "simple_value_with_quotes",
args: args{
s: utils.Ptr("\"mytest\""),
},
want: utils.Ptr("mytest"),
},
{
name: "simple_values_with_quotes",
args: args{
s: utils.Ptr("\"my test here\""),
},
want: utils.Ptr("my test here"),
},
{
name: "simple_values",
args: args{
s: utils.Ptr("my test here"),
},
want: utils.Ptr("my test here"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cleanString(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("cleanString() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -4,6 +4,8 @@ package postgresflexalpha
import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@ -12,11 +14,23 @@ import (
func DatabaseResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"database_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The ID of the database.",
MarkdownDescription: "The ID of the database.",
},
"id": schema.Int64Attribute{
Computed: true,
Description: "The id of the database.",
MarkdownDescription: "The id of the database.",
},
"instance_id": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The ID of the instance.",
MarkdownDescription: "The ID of the instance.",
},
"name": schema.StringAttribute{
Required: true,
Description: "The name of the database.",
@ -28,12 +42,33 @@ func DatabaseResourceSchema(ctx context.Context) schema.Schema {
Description: "The owner of the database.",
MarkdownDescription: "The owner of the database.",
},
"project_id": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The STACKIT project ID.",
MarkdownDescription: "The STACKIT project ID.",
},
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The region which should be addressed",
MarkdownDescription: "The region which should be addressed",
Validators: []validator.String{
stringvalidator.OneOf(
"eu01",
),
},
},
},
}
}
type DatabaseModel struct {
Id types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`
DatabaseId types.Int64 `tfsdk:"database_id"`
Id types.Int64 `tfsdk:"id"`
InstanceId types.String `tfsdk:"instance_id"`
Name types.String `tfsdk:"name"`
Owner types.String `tfsdk:"owner"`
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
}