From f44659f703ac94a4aec69393b2e85bf730c81f7b Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 9 Feb 2026 15:12:45 +0100 Subject: [PATCH 01/31] feat: refactor builder for datasource --- cmd/cmd/build/build.go | 76 +- .../data-sources/postgresflexalpha_flavors.md | 2 +- .../postgresflexalpha_instance.md | 2 +- .../sqlserverflexalpha_database.md | 2 +- .../sqlserverflexalpha_instance.md | 2 +- .../sqlserverflexbeta_instance.md | 2 +- .../resources_gen/database_resource_gen.go | 41 +- .../flavors_data_source_gen.go | 2 +- .../instance_data_source_gen.go | 4 +- .../postgresflex_acc_test.go | 5 + .../postgresflexalpha/user/datasource.go | 3 - .../datasources_gen/user_data_source_gen.go | 36 +- .../postgresflexalpha/user/resource.go | 3 - .../user/resources_gen/user_resource_gen.go | 12 - .../database_data_source_gen.go | 4 +- .../flavors_data_source_gen.go | 2 +- .../instance_data_source_gen.go | 4 +- .../datasources_gen/user_data_source_gen.go | 2 +- .../database_data_source_fix.go | 81 -- .../databases_data_source_gen.go | 1180 ----------------- .../resources_gen/database_resource_gen.go | 99 -- .../instance_data_source_gen.go | 4 +- .../instance/resource_test.go | 7 + .../datasources_gen/user_data_source_gen.go | 2 +- 24 files changed, 151 insertions(+), 1426 deletions(-) delete mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go delete mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go delete mode 100644 stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go diff --git a/cmd/cmd/build/build.go b/cmd/cmd/build/build.go index f210b8d2..30fbf552 100644 --- a/cmd/cmd/build/build.go +++ b/cmd/cmd/build/build.go @@ -1,6 +1,7 @@ package build import ( + "bufio" "bytes" "errors" "fmt" @@ -509,7 +510,7 @@ func generateServiceFiles(rootDir, generatorDir string) error { oasFile := path.Join(generatorDir, "oas", fmt.Sprintf("%s%s.json", service.Name(), svcVersion.Name())) if _, oasErr := os.Stat(oasFile); os.IsNotExist(oasErr) { - slog.Warn(" coulc not find matching oas", "svc", service.Name(), "version", svcVersion.Name()) + slog.Warn(" could not find matching oas", "svc", service.Name(), "version", svcVersion.Name()) continue } @@ -648,6 +649,15 @@ func generateServiceFiles(rootDir, generatorDir string) error { return err } } + + tfAnoErr := handleTfTagForDatasourceFile( + path.Join(tgtFolder, fmt.Sprintf("%s_data_source_gen.go", resource)), + scName, + resource, + ) + if tfAnoErr != nil { + return tfAnoErr + } } } } @@ -655,6 +665,70 @@ func generateServiceFiles(rootDir, generatorDir string) error { return nil } +// handleTfTagForDatasourceFile replaces existing "id" with "stf_original_api_id" +func handleTfTagForDatasourceFile(filePath, service, resource string) error { + slog.Info(" handle terraform tag for datasource", "service", service, "resource", resource) + if !fileExists(filePath) { + slog.Warn(" could not find file, skipping", "path", filePath) + return nil + } + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + tmp, err := os.CreateTemp("", "replace-*") + if err != nil { + return err + } + defer tmp.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + resLine, err := handleLine(sc.Text()) + if err != nil { + return err + } + if _, err := io.WriteString(tmp, resLine+"\n"); err != nil { + return err + } + } + if scErr := sc.Err(); scErr != nil { + return scErr + } + + if err := tmp.Close(); err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + if err := os.Rename(tmp.Name(), filePath); err != nil { + log.Fatal(err) + } + return nil +} + +func handleLine(line string) (string, error) { + schemaRegex := regexp.MustCompile(`(\s+")(id)(": schema.[a-zA-Z0-9]+Attribute{)`) + + schemaMatches := schemaRegex.FindAllStringSubmatch(line, -1) + if schemaMatches != nil { + return fmt.Sprintf("%stf_original_api_id%s", schemaMatches[0][1], schemaMatches[0][3]), nil + } + + modelRegex := regexp.MustCompile(`(\s+Id\s+types.[a-zA-Z0-9]+\s+.tfsdk:")(id)(".)`) + modelMatches := modelRegex.FindAllStringSubmatch(line, -1) + if modelMatches != nil { + return fmt.Sprintf("%stf_original_api_id%s", modelMatches[0][1], modelMatches[0][3]), nil + } + + return line, nil +} + func checkCommands(commands []string) error { for _, commandName := range commands { if !commandExists(commandName) { diff --git a/docs/data-sources/postgresflexalpha_flavors.md b/docs/data-sources/postgresflexalpha_flavors.md index f90ae257..06645bb4 100644 --- a/docs/data-sources/postgresflexalpha_flavors.md +++ b/docs/data-sources/postgresflexalpha_flavors.md @@ -38,12 +38,12 @@ Read-Only: - `cpu` (Number) The cpu count of the instance. - `description` (String) The flavor description. -- `id` (String) The id of the instance flavor. - `max_gb` (Number) maximum storage which can be ordered for the flavor in Gigabyte. - `memory` (Number) The memory of the instance in Gibibyte. - `min_gb` (Number) minimum storage which is required to order in Gigabyte. - `node_type` (String) defines the nodeType it can be either single or replica - `storage_classes` (Attributes List) maximum storage which can be ordered for the flavor in Gigabyte. (see [below for nested schema](#nestedatt--flavors--storage_classes)) +- `tf_original_api_id` (String) The id of the instance flavor. ### Nested Schema for `flavors.storage_classes` diff --git a/docs/data-sources/postgresflexalpha_instance.md b/docs/data-sources/postgresflexalpha_instance.md index 54d887ea..466745a6 100644 --- a/docs/data-sources/postgresflexalpha_instance.md +++ b/docs/data-sources/postgresflexalpha_instance.md @@ -37,7 +37,6 @@ data "stackitprivatepreview_postgresflexalpha_instance" "example" { ⚠︝ **Note:** This feature is in private preview. Supplying this object is only permitted for enabled accounts. If your account does not have access, the request will be rejected. (see [below for nested schema](#nestedatt--encryption)) - `flavor_id` (String) The id of the instance flavor. -- `id` (String) The ID of the instance. - `is_deletable` (Boolean) Whether the instance can be deleted or not. - `name` (String) The name of the instance. - `network` (Attributes) The access configuration of the instance (see [below for nested schema](#nestedatt--network)) @@ -45,6 +44,7 @@ data "stackitprivatepreview_postgresflexalpha_instance" "example" { - `retention_days` (Number) How long backups are retained. The value can only be between 32 and 365 days. - `status` (String) The current status of the instance. - `storage` (Attributes) The object containing information about the storage size and class. (see [below for nested schema](#nestedatt--storage)) +- `tf_original_api_id` (String) The ID of the instance. - `version` (String) The Postgres version used for the instance. See [Versions Endpoint](/documentation/postgres-flex-service/version/v3alpha1#tag/Version) for supported version parameters. diff --git a/docs/data-sources/sqlserverflexalpha_database.md b/docs/data-sources/sqlserverflexalpha_database.md index 4aab99cc..5db648f4 100644 --- a/docs/data-sources/sqlserverflexalpha_database.md +++ b/docs/data-sources/sqlserverflexalpha_database.md @@ -26,6 +26,6 @@ description: |- - `collation_name` (String) The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint. - `compatibility_level` (Number) CompatibilityLevel of the Database. -- `id` (Number) The id of the database. - `name` (String) The name of the database. - `owner` (String) The owner of the database. +- `tf_original_api_id` (Number) The id of the database. diff --git a/docs/data-sources/sqlserverflexalpha_instance.md b/docs/data-sources/sqlserverflexalpha_instance.md index 134eb567..b05d7b8e 100644 --- a/docs/data-sources/sqlserverflexalpha_instance.md +++ b/docs/data-sources/sqlserverflexalpha_instance.md @@ -34,7 +34,6 @@ data "stackitprivatepreview_sqlserverflexalpha_instance" "example" { - `edition` (String) Edition of the MSSQL server instance - `encryption` (Attributes) this defines which key to use for storage encryption (see [below for nested schema](#nestedatt--encryption)) - `flavor_id` (String) The id of the instance flavor. -- `id` (String) The ID of the instance. - `is_deletable` (Boolean) Whether the instance can be deleted or not. - `name` (String) The name of the instance. - `network` (Attributes) The access configuration of the instance (see [below for nested schema](#nestedatt--network)) @@ -42,6 +41,7 @@ data "stackitprivatepreview_sqlserverflexalpha_instance" "example" { - `retention_days` (Number) The days for how long the backup files should be stored before cleaned up. 30 to 365 - `status` (String) - `storage` (Attributes) The object containing information about the storage size and class. (see [below for nested schema](#nestedatt--storage)) +- `tf_original_api_id` (String) The ID of the instance. - `version` (String) The sqlserver version used for the instance. diff --git a/docs/data-sources/sqlserverflexbeta_instance.md b/docs/data-sources/sqlserverflexbeta_instance.md index cc3645ef..431f95f1 100644 --- a/docs/data-sources/sqlserverflexbeta_instance.md +++ b/docs/data-sources/sqlserverflexbeta_instance.md @@ -34,7 +34,6 @@ data "stackitprivatepreview_sqlserverflexbeta_instance" "example" { - `edition` (String) Edition of the MSSQL server instance - `encryption` (Attributes) this defines which key to use for storage encryption (see [below for nested schema](#nestedatt--encryption)) - `flavor_id` (String) The id of the instance flavor. -- `id` (String) The ID of the instance. - `is_deletable` (Boolean) Whether the instance can be deleted or not. - `name` (String) The name of the instance. - `network` (Attributes) The access configuration of the instance (see [below for nested schema](#nestedatt--network)) @@ -42,6 +41,7 @@ data "stackitprivatepreview_sqlserverflexbeta_instance" "example" { - `retention_days` (Number) The days for how long the backup files should be stored before cleaned up. 30 to 365 - `status` (String) - `storage` (Attributes) The object containing information about the storage size and class. (see [below for nested schema](#nestedatt--storage)) +- `tf_original_api_id` (String) The ID of the instance. - `version` (String) The sqlserver version used for the instance. diff --git a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go index 95f6b6e5..6affc956 100644 --- a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go @@ -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"` } diff --git a/stackit/internal/services/postgresflexalpha/flavors/datasources_gen/flavors_data_source_gen.go b/stackit/internal/services/postgresflexalpha/flavors/datasources_gen/flavors_data_source_gen.go index 924d1375..dbfe5cc9 100644 --- a/stackit/internal/services/postgresflexalpha/flavors/datasources_gen/flavors_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/flavors/datasources_gen/flavors_data_source_gen.go @@ -33,7 +33,7 @@ func FlavorsDataSourceSchema(ctx context.Context) schema.Schema { Description: "The flavor description.", MarkdownDescription: "The flavor description.", }, - "id": schema.StringAttribute{ + "tf_original_api_id": schema.StringAttribute{ Computed: true, Description: "The id of the instance flavor.", MarkdownDescription: "The id of the instance flavor.", diff --git a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go index 5ff386fe..047d0176 100644 --- a/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/instance/datasources_gen/instance_data_source_gen.go @@ -88,7 +88,7 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema { Description: "The id of the instance flavor.", MarkdownDescription: "The id of the instance flavor.", }, - "id": schema.StringAttribute{ + "tf_original_api_id": schema.StringAttribute{ Computed: true, Description: "The ID of the instance.", MarkdownDescription: "The ID of the instance.", @@ -204,7 +204,7 @@ type InstanceModel struct { ConnectionInfo ConnectionInfoValue `tfsdk:"connection_info"` Encryption EncryptionValue `tfsdk:"encryption"` FlavorId types.String `tfsdk:"flavor_id"` - Id types.String `tfsdk:"id"` + Id types.String `tfsdk:"tf_original_api_id"` InstanceId types.String `tfsdk:"instance_id"` IsDeletable types.Bool `tfsdk:"is_deletable"` Name types.String `tfsdk:"name"` diff --git a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go index 2a8a12b5..fbc442e4 100644 --- a/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go +++ b/stackit/internal/services/postgresflexalpha/postgresflex_acc_test.go @@ -164,6 +164,11 @@ func TestAccPostgresFlexFlexResource(t *testing.T) { Steps: []resource.TestStep{ // Creation { + // testdata/ + // ConfigDirectory: config.TestNameDirectory(), + + // testdata// + // ConfigDirectory: config.TestStepDirectory(), Config: configResources(instanceResource["backup_schedule"], &testutil.Region), Check: resource.ComposeAggregateTestCheckFunc( // Instance diff --git a/stackit/internal/services/postgresflexalpha/user/datasource.go b/stackit/internal/services/postgresflexalpha/user/datasource.go index 70d05aba..7b3a1428 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasource.go +++ b/stackit/internal/services/postgresflexalpha/user/datasource.go @@ -274,10 +274,7 @@ func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSour } model.Roles = rolesSet } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) model.Status = types.StringPointerValue(user.Status) - model.ConnectionString = types.StringPointerValue(user.ConnectionString) return nil } diff --git a/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go b/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go index fb2a7644..29a7cca0 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/user/datasources_gen/user_data_source_gen.go @@ -14,17 +14,7 @@ import ( func UserDataSourceSchema(ctx context.Context) schema.Schema { return schema.Schema{ Attributes: map[string]schema.Attribute{ - "connection_string": schema.StringAttribute{ - Computed: true, - Description: "The connection string for the user to the instance.", - MarkdownDescription: "The connection string for the user to the instance.", - }, - "host": schema.StringAttribute{ - Computed: true, - Description: "The host of the instance in which the user belongs to.", - MarkdownDescription: "The host of the instance in which the user belongs to.", - }, - "id": schema.Int64Attribute{ + "tf_original_api_id": schema.Int64Attribute{ Computed: true, Description: "The ID of the user.", MarkdownDescription: "The ID of the user.", @@ -39,11 +29,6 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema { Description: "The name of the user.", MarkdownDescription: "The name of the user.", }, - "port": schema.Int64Attribute{ - Computed: true, - Description: "The port of the instance in which the user belongs to.", - MarkdownDescription: "The port of the instance in which the user belongs to.", - }, "project_id": schema.StringAttribute{ Required: true, Description: "The STACKIT project ID.", @@ -80,15 +65,12 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema { } type UserModel struct { - ConnectionString types.String `tfsdk:"connection_string"` - Host types.String `tfsdk:"host"` - Id types.Int64 `tfsdk:"id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - Port types.Int64 `tfsdk:"port"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - Roles types.List `tfsdk:"roles"` - Status types.String `tfsdk:"status"` - UserId types.Int64 `tfsdk:"user_id"` + Id types.Int64 `tfsdk:"tf_original_api_id"` + InstanceId types.String `tfsdk:"instance_id"` + Name types.String `tfsdk:"name"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Roles types.List `tfsdk:"roles"` + Status types.String `tfsdk:"status"` + UserId types.Int64 `tfsdk:"user_id"` } diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 4df9577d..3aa5f519 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -556,11 +556,8 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri } model.Roles = rolesSet } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) model.Status = types.StringPointerValue(user.Status) - model.ConnectionString = types.StringPointerValue(user.ConnectionString) return nil } diff --git a/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go b/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go index 9734c2a9..f07ab701 100644 --- a/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/user/resources_gen/user_resource_gen.go @@ -19,11 +19,6 @@ func UserResourceSchema(ctx context.Context) schema.Schema { Description: "The connection string for the user to the instance.", MarkdownDescription: "The connection string for the user to the instance.", }, - "host": schema.StringAttribute{ - Computed: true, - Description: "The host of the instance in which the user belongs to.", - MarkdownDescription: "The host of the instance in which the user belongs to.", - }, "id": schema.Int64Attribute{ Computed: true, Description: "The ID of the user.", @@ -45,11 +40,6 @@ func UserResourceSchema(ctx context.Context) schema.Schema { Description: "The password for the user.", MarkdownDescription: "The password for the user.", }, - "port": schema.Int64Attribute{ - Computed: true, - Description: "The port of the instance in which the user belongs to.", - MarkdownDescription: "The port of the instance in which the user belongs to.", - }, "project_id": schema.StringAttribute{ Optional: true, Computed: true, @@ -91,12 +81,10 @@ func UserResourceSchema(ctx context.Context) schema.Schema { type UserModel struct { ConnectionString types.String `tfsdk:"connection_string"` - Host types.String `tfsdk:"host"` Id types.Int64 `tfsdk:"id"` InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` ProjectId types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` Roles types.List `tfsdk:"roles"` diff --git a/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go index 25406f5f..82250802 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go @@ -29,7 +29,7 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { Description: "The name of the database.", MarkdownDescription: "The name of the database.", }, - "id": schema.Int64Attribute{ + "tf_original_api_id": schema.Int64Attribute{ Computed: true, Description: "The id of the database.", MarkdownDescription: "The id of the database.", @@ -72,7 +72,7 @@ type DatabaseModel struct { CollationName types.String `tfsdk:"collation_name"` CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` DatabaseName types.String `tfsdk:"database_name"` - Id types.Int64 `tfsdk:"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"` diff --git a/stackit/internal/services/sqlserverflexalpha/flavors/datasources_gen/flavors_data_source_gen.go b/stackit/internal/services/sqlserverflexalpha/flavors/datasources_gen/flavors_data_source_gen.go index 43ac64f5..40f086e2 100644 --- a/stackit/internal/services/sqlserverflexalpha/flavors/datasources_gen/flavors_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexalpha/flavors/datasources_gen/flavors_data_source_gen.go @@ -33,7 +33,7 @@ func FlavorsDataSourceSchema(ctx context.Context) schema.Schema { Description: "The flavor description.", MarkdownDescription: "The flavor description.", }, - "id": schema.StringAttribute{ + "tf_original_api_id": schema.StringAttribute{ Computed: true, Description: "The id of the instance flavor.", MarkdownDescription: "The id of the instance flavor.", diff --git a/stackit/internal/services/sqlserverflexalpha/instance/datasources_gen/instance_data_source_gen.go b/stackit/internal/services/sqlserverflexalpha/instance/datasources_gen/instance_data_source_gen.go index dcf7f6dd..5880a392 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/datasources_gen/instance_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/datasources_gen/instance_data_source_gen.go @@ -65,7 +65,7 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema { Description: "The id of the instance flavor.", MarkdownDescription: "The id of the instance flavor.", }, - "id": schema.StringAttribute{ + "tf_original_api_id": schema.StringAttribute{ Computed: true, Description: "The ID of the instance.", MarkdownDescription: "The ID of the instance.", @@ -178,7 +178,7 @@ type InstanceModel struct { Edition types.String `tfsdk:"edition"` Encryption EncryptionValue `tfsdk:"encryption"` FlavorId types.String `tfsdk:"flavor_id"` - Id types.String `tfsdk:"id"` + Id types.String `tfsdk:"tf_original_api_id"` InstanceId types.String `tfsdk:"instance_id"` IsDeletable types.Bool `tfsdk:"is_deletable"` Name types.String `tfsdk:"name"` diff --git a/stackit/internal/services/sqlserverflexalpha/user/datasources_gen/user_data_source_gen.go b/stackit/internal/services/sqlserverflexalpha/user/datasources_gen/user_data_source_gen.go index 3d252237..329469ea 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/datasources_gen/user_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexalpha/user/datasources_gen/user_data_source_gen.go @@ -98,7 +98,7 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema { "users": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "id": schema.Int64Attribute{ + "tf_original_api_id": schema.Int64Attribute{ Computed: true, Description: "The ID of the user.", MarkdownDescription: "The ID of the user.", diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go deleted file mode 100644 index 92b1064e..00000000 --- a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_fix.go +++ /dev/null @@ -1,81 +0,0 @@ -// Code generated by terraform-plugin-framework-generator DO NOT EDIT. - -package sqlserverflexbeta - -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{ - "collation_name": schema.StringAttribute{ - Computed: true, - Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - }, - "compatibility_level": schema.Int64Attribute{ - Computed: true, - Description: "CompatibilityLevel of the Database.", - MarkdownDescription: "CompatibilityLevel of the Database.", - }, - "database_name": schema.StringAttribute{ - Required: true, - Description: "The name of the database.", - MarkdownDescription: "The name 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 { - CollationName types.String `tfsdk:"collation_name"` - CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` - DatabaseName types.String `tfsdk:"database_name"` - 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"` -} diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go deleted file mode 100644 index 71ec8fb4..00000000 --- a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go +++ /dev/null @@ -1,1180 +0,0 @@ -// Code generated by terraform-plugin-framework-generator DO NOT EDIT. - -package sqlserverflexbeta - -import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" -) - -func DatabasesDataSourceSchema(ctx context.Context) schema.Schema { - return schema.Schema{ - Attributes: map[string]schema.Attribute{ - "databases": schema.ListNestedAttribute{ - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "created": schema.StringAttribute{ - Computed: true, - Description: "The date when the database was created in RFC3339 format.", - MarkdownDescription: "The date when the database was created in RFC3339 format.", - }, - "id": schema.Int64Attribute{ - Computed: true, - Description: "The id of the database.", - MarkdownDescription: "The id of the database.", - }, - "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.", - }, - }, - CustomType: DatabasesType{ - ObjectType: types.ObjectType{ - AttrTypes: DatabasesValue{}.AttributeTypes(ctx), - }, - }, - }, - Computed: true, - Description: "A list containing all databases for the instance.", - MarkdownDescription: "A list containing all databases for the instance.", - }, - "instance_id": schema.StringAttribute{ - Required: true, - Description: "The ID of the instance.", - MarkdownDescription: "The ID of the instance.", - }, - "page": schema.Int64Attribute{ - Optional: true, - Computed: true, - Description: "Number of the page of items list to be returned.", - MarkdownDescription: "Number of the page of items list to be returned.", - }, - "pagination": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "page": schema.Int64Attribute{ - Computed: true, - }, - "size": schema.Int64Attribute{ - Computed: true, - }, - "sort": schema.StringAttribute{ - Computed: true, - }, - "total_pages": schema.Int64Attribute{ - Computed: true, - }, - "total_rows": schema.Int64Attribute{ - Computed: true, - }, - }, - CustomType: PaginationType{ - ObjectType: types.ObjectType{ - AttrTypes: PaginationValue{}.AttributeTypes(ctx), - }, - }, - Computed: true, - }, - "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", - ), - }, - }, - "size": schema.Int64Attribute{ - Optional: true, - Computed: true, - Description: "Number of items to be returned on each page.", - MarkdownDescription: "Number of items to be returned on each page.", - }, - "sort": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "Sorting of the databases to be returned on each page.", - MarkdownDescription: "Sorting of the databases to be returned on each page.", - Validators: []validator.String{ - stringvalidator.OneOf( - "created_at.desc", - "created_at.asc", - "database_id.desc", - "database_id.asc", - "database_name.desc", - "database_name.asc", - "database_owner.desc", - "database_owner.asc", - "index.asc", - "index.desc", - ), - }, - }, - }, - } -} - -type DatabasesModel struct { - Databases types.List `tfsdk:"databases"` - InstanceId types.String `tfsdk:"instance_id"` - Page types.Int64 `tfsdk:"page"` - Pagination PaginationValue `tfsdk:"pagination"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - Size types.Int64 `tfsdk:"size"` - Sort types.String `tfsdk:"sort"` -} - -var _ basetypes.ObjectTypable = DatabasesType{} - -type DatabasesType struct { - basetypes.ObjectType -} - -func (t DatabasesType) Equal(o attr.Type) bool { - other, ok := o.(DatabasesType) - - if !ok { - return false - } - - return t.ObjectType.Equal(other.ObjectType) -} - -func (t DatabasesType) String() string { - return "DatabasesType" -} - -func (t DatabasesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { - var diags diag.Diagnostics - - attributes := in.Attributes() - - createdAttribute, ok := attributes["created"] - - if !ok { - diags.AddError( - "Attribute Missing", - `created is missing from object`) - - return nil, diags - } - - createdVal, ok := createdAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`created expected to be basetypes.StringValue, was: %T`, createdAttribute)) - } - - idAttribute, ok := attributes["id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `id is missing from object`) - - return nil, diags - } - - idVal, ok := idAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute)) - } - - nameAttribute, ok := attributes["name"] - - if !ok { - diags.AddError( - "Attribute Missing", - `name is missing from object`) - - return nil, diags - } - - nameVal, ok := nameAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute)) - } - - ownerAttribute, ok := attributes["owner"] - - if !ok { - diags.AddError( - "Attribute Missing", - `owner is missing from object`) - - return nil, diags - } - - ownerVal, ok := ownerAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute)) - } - - if diags.HasError() { - return nil, diags - } - - return DatabasesValue{ - Created: createdVal, - Id: idVal, - Name: nameVal, - Owner: ownerVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewDatabasesValueNull() DatabasesValue { - return DatabasesValue{ - state: attr.ValueStateNull, - } -} - -func NewDatabasesValueUnknown() DatabasesValue { - return DatabasesValue{ - state: attr.ValueStateUnknown, - } -} - -func NewDatabasesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (DatabasesValue, diag.Diagnostics) { - var diags diag.Diagnostics - - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 - ctx := context.Background() - - for name, attributeType := range attributeTypes { - attribute, ok := attributes[name] - - if !ok { - diags.AddError( - "Missing DatabasesValue Attribute Value", - "While creating a DatabasesValue value, a missing attribute value was detected. "+ - "A DatabasesValue must contain values for all attributes, even if null or unknown. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("DatabasesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), - ) - - continue - } - - if !attributeType.Equal(attribute.Type(ctx)) { - diags.AddError( - "Invalid DatabasesValue Attribute Type", - "While creating a DatabasesValue value, an invalid attribute value was detected. "+ - "A DatabasesValue must use a matching attribute type for the value. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("DatabasesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ - fmt.Sprintf("DatabasesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), - ) - } - } - - for name := range attributes { - _, ok := attributeTypes[name] - - if !ok { - diags.AddError( - "Extra DatabasesValue Attribute Value", - "While creating a DatabasesValue value, an extra attribute value was detected. "+ - "A DatabasesValue must not contain values beyond the expected attribute types. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Extra DatabasesValue Attribute Name: %s", name), - ) - } - } - - if diags.HasError() { - return NewDatabasesValueUnknown(), diags - } - - createdAttribute, ok := attributes["created"] - - if !ok { - diags.AddError( - "Attribute Missing", - `created is missing from object`) - - return NewDatabasesValueUnknown(), diags - } - - createdVal, ok := createdAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`created expected to be basetypes.StringValue, was: %T`, createdAttribute)) - } - - idAttribute, ok := attributes["id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `id is missing from object`) - - return NewDatabasesValueUnknown(), diags - } - - idVal, ok := idAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute)) - } - - nameAttribute, ok := attributes["name"] - - if !ok { - diags.AddError( - "Attribute Missing", - `name is missing from object`) - - return NewDatabasesValueUnknown(), diags - } - - nameVal, ok := nameAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute)) - } - - ownerAttribute, ok := attributes["owner"] - - if !ok { - diags.AddError( - "Attribute Missing", - `owner is missing from object`) - - return NewDatabasesValueUnknown(), diags - } - - ownerVal, ok := ownerAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute)) - } - - if diags.HasError() { - return NewDatabasesValueUnknown(), diags - } - - return DatabasesValue{ - Created: createdVal, - Id: idVal, - Name: nameVal, - Owner: ownerVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewDatabasesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) DatabasesValue { - object, diags := NewDatabasesValue(attributeTypes, attributes) - - if diags.HasError() { - // This could potentially be added to the diag package. - diagsStrings := make([]string, 0, len(diags)) - - for _, diagnostic := range diags { - diagsStrings = append(diagsStrings, fmt.Sprintf( - "%s | %s | %s", - diagnostic.Severity(), - diagnostic.Summary(), - diagnostic.Detail())) - } - - panic("NewDatabasesValueMust received error(s): " + strings.Join(diagsStrings, "\n")) - } - - return object -} - -func (t DatabasesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { - if in.Type() == nil { - return NewDatabasesValueNull(), nil - } - - if !in.Type().Equal(t.TerraformType(ctx)) { - return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) - } - - if !in.IsKnown() { - return NewDatabasesValueUnknown(), nil - } - - if in.IsNull() { - return NewDatabasesValueNull(), nil - } - - attributes := map[string]attr.Value{} - - val := map[string]tftypes.Value{} - - err := in.As(&val) - - if err != nil { - return nil, err - } - - for k, v := range val { - a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) - - if err != nil { - return nil, err - } - - attributes[k] = a - } - - return NewDatabasesValueMust(DatabasesValue{}.AttributeTypes(ctx), attributes), nil -} - -func (t DatabasesType) ValueType(ctx context.Context) attr.Value { - return DatabasesValue{} -} - -var _ basetypes.ObjectValuable = DatabasesValue{} - -type DatabasesValue struct { - Created basetypes.StringValue `tfsdk:"created"` - Id basetypes.Int64Value `tfsdk:"id"` - Name basetypes.StringValue `tfsdk:"name"` - Owner basetypes.StringValue `tfsdk:"owner"` - state attr.ValueState -} - -func (v DatabasesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - attrTypes := make(map[string]tftypes.Type, 4) - - var val tftypes.Value - var err error - - attrTypes["created"] = basetypes.StringType{}.TerraformType(ctx) - attrTypes["id"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx) - attrTypes["owner"] = basetypes.StringType{}.TerraformType(ctx) - - objectType := tftypes.Object{AttributeTypes: attrTypes} - - switch v.state { - case attr.ValueStateKnown: - vals := make(map[string]tftypes.Value, 4) - - val, err = v.Created.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["created"] = val - - val, err = v.Id.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["id"] = val - - val, err = v.Name.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["name"] = val - - val, err = v.Owner.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["owner"] = val - - if err := tftypes.ValidateValue(objectType, vals); err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - return tftypes.NewValue(objectType, vals), nil - case attr.ValueStateNull: - return tftypes.NewValue(objectType, nil), nil - case attr.ValueStateUnknown: - return tftypes.NewValue(objectType, tftypes.UnknownValue), nil - default: - panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) - } -} - -func (v DatabasesValue) IsNull() bool { - return v.state == attr.ValueStateNull -} - -func (v DatabasesValue) IsUnknown() bool { - return v.state == attr.ValueStateUnknown -} - -func (v DatabasesValue) String() string { - return "DatabasesValue" -} - -func (v DatabasesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { - var diags diag.Diagnostics - - attributeTypes := map[string]attr.Type{ - "created": basetypes.StringType{}, - "id": basetypes.Int64Type{}, - "name": basetypes.StringType{}, - "owner": basetypes.StringType{}, - } - - if v.IsNull() { - return types.ObjectNull(attributeTypes), diags - } - - if v.IsUnknown() { - return types.ObjectUnknown(attributeTypes), diags - } - - objVal, diags := types.ObjectValue( - attributeTypes, - map[string]attr.Value{ - "created": v.Created, - "id": v.Id, - "name": v.Name, - "owner": v.Owner, - }) - - return objVal, diags -} - -func (v DatabasesValue) Equal(o attr.Value) bool { - other, ok := o.(DatabasesValue) - - if !ok { - return false - } - - if v.state != other.state { - return false - } - - if v.state != attr.ValueStateKnown { - return true - } - - if !v.Created.Equal(other.Created) { - return false - } - - if !v.Id.Equal(other.Id) { - return false - } - - if !v.Name.Equal(other.Name) { - return false - } - - if !v.Owner.Equal(other.Owner) { - return false - } - - return true -} - -func (v DatabasesValue) Type(ctx context.Context) attr.Type { - return DatabasesType{ - basetypes.ObjectType{ - AttrTypes: v.AttributeTypes(ctx), - }, - } -} - -func (v DatabasesValue) AttributeTypes(ctx context.Context) map[string]attr.Type { - return map[string]attr.Type{ - "created": basetypes.StringType{}, - "id": basetypes.Int64Type{}, - "name": basetypes.StringType{}, - "owner": basetypes.StringType{}, - } -} - -var _ basetypes.ObjectTypable = PaginationType{} - -type PaginationType struct { - basetypes.ObjectType -} - -func (t PaginationType) Equal(o attr.Type) bool { - other, ok := o.(PaginationType) - - if !ok { - return false - } - - return t.ObjectType.Equal(other.ObjectType) -} - -func (t PaginationType) String() string { - return "PaginationType" -} - -func (t PaginationType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { - var diags diag.Diagnostics - - attributes := in.Attributes() - - pageAttribute, ok := attributes["page"] - - if !ok { - diags.AddError( - "Attribute Missing", - `page is missing from object`) - - return nil, diags - } - - pageVal, ok := pageAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) - } - - sizeAttribute, ok := attributes["size"] - - if !ok { - diags.AddError( - "Attribute Missing", - `size is missing from object`) - - return nil, diags - } - - sizeVal, ok := sizeAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) - } - - sortAttribute, ok := attributes["sort"] - - if !ok { - diags.AddError( - "Attribute Missing", - `sort is missing from object`) - - return nil, diags - } - - sortVal, ok := sortAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) - } - - totalPagesAttribute, ok := attributes["total_pages"] - - if !ok { - diags.AddError( - "Attribute Missing", - `total_pages is missing from object`) - - return nil, diags - } - - totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) - } - - totalRowsAttribute, ok := attributes["total_rows"] - - if !ok { - diags.AddError( - "Attribute Missing", - `total_rows is missing from object`) - - return nil, diags - } - - totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) - } - - if diags.HasError() { - return nil, diags - } - - return PaginationValue{ - Page: pageVal, - Size: sizeVal, - Sort: sortVal, - TotalPages: totalPagesVal, - TotalRows: totalRowsVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewPaginationValueNull() PaginationValue { - return PaginationValue{ - state: attr.ValueStateNull, - } -} - -func NewPaginationValueUnknown() PaginationValue { - return PaginationValue{ - state: attr.ValueStateUnknown, - } -} - -func NewPaginationValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (PaginationValue, diag.Diagnostics) { - var diags diag.Diagnostics - - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 - ctx := context.Background() - - for name, attributeType := range attributeTypes { - attribute, ok := attributes[name] - - if !ok { - diags.AddError( - "Missing PaginationValue Attribute Value", - "While creating a PaginationValue value, a missing attribute value was detected. "+ - "A PaginationValue must contain values for all attributes, even if null or unknown. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), - ) - - continue - } - - if !attributeType.Equal(attribute.Type(ctx)) { - diags.AddError( - "Invalid PaginationValue Attribute Type", - "While creating a PaginationValue value, an invalid attribute value was detected. "+ - "A PaginationValue must use a matching attribute type for the value. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ - fmt.Sprintf("PaginationValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), - ) - } - } - - for name := range attributes { - _, ok := attributeTypes[name] - - if !ok { - diags.AddError( - "Extra PaginationValue Attribute Value", - "While creating a PaginationValue value, an extra attribute value was detected. "+ - "A PaginationValue must not contain values beyond the expected attribute types. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Extra PaginationValue Attribute Name: %s", name), - ) - } - } - - if diags.HasError() { - return NewPaginationValueUnknown(), diags - } - - pageAttribute, ok := attributes["page"] - - if !ok { - diags.AddError( - "Attribute Missing", - `page is missing from object`) - - return NewPaginationValueUnknown(), diags - } - - pageVal, ok := pageAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) - } - - sizeAttribute, ok := attributes["size"] - - if !ok { - diags.AddError( - "Attribute Missing", - `size is missing from object`) - - return NewPaginationValueUnknown(), diags - } - - sizeVal, ok := sizeAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) - } - - sortAttribute, ok := attributes["sort"] - - if !ok { - diags.AddError( - "Attribute Missing", - `sort is missing from object`) - - return NewPaginationValueUnknown(), diags - } - - sortVal, ok := sortAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) - } - - totalPagesAttribute, ok := attributes["total_pages"] - - if !ok { - diags.AddError( - "Attribute Missing", - `total_pages is missing from object`) - - return NewPaginationValueUnknown(), diags - } - - totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) - } - - totalRowsAttribute, ok := attributes["total_rows"] - - if !ok { - diags.AddError( - "Attribute Missing", - `total_rows is missing from object`) - - return NewPaginationValueUnknown(), diags - } - - totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) - } - - if diags.HasError() { - return NewPaginationValueUnknown(), diags - } - - return PaginationValue{ - Page: pageVal, - Size: sizeVal, - Sort: sortVal, - TotalPages: totalPagesVal, - TotalRows: totalRowsVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewPaginationValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) PaginationValue { - object, diags := NewPaginationValue(attributeTypes, attributes) - - if diags.HasError() { - // This could potentially be added to the diag package. - diagsStrings := make([]string, 0, len(diags)) - - for _, diagnostic := range diags { - diagsStrings = append(diagsStrings, fmt.Sprintf( - "%s | %s | %s", - diagnostic.Severity(), - diagnostic.Summary(), - diagnostic.Detail())) - } - - panic("NewPaginationValueMust received error(s): " + strings.Join(diagsStrings, "\n")) - } - - return object -} - -func (t PaginationType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { - if in.Type() == nil { - return NewPaginationValueNull(), nil - } - - if !in.Type().Equal(t.TerraformType(ctx)) { - return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) - } - - if !in.IsKnown() { - return NewPaginationValueUnknown(), nil - } - - if in.IsNull() { - return NewPaginationValueNull(), nil - } - - attributes := map[string]attr.Value{} - - val := map[string]tftypes.Value{} - - err := in.As(&val) - - if err != nil { - return nil, err - } - - for k, v := range val { - a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) - - if err != nil { - return nil, err - } - - attributes[k] = a - } - - return NewPaginationValueMust(PaginationValue{}.AttributeTypes(ctx), attributes), nil -} - -func (t PaginationType) ValueType(ctx context.Context) attr.Value { - return PaginationValue{} -} - -var _ basetypes.ObjectValuable = PaginationValue{} - -type PaginationValue struct { - Page basetypes.Int64Value `tfsdk:"page"` - Size basetypes.Int64Value `tfsdk:"size"` - Sort basetypes.StringValue `tfsdk:"sort"` - TotalPages basetypes.Int64Value `tfsdk:"total_pages"` - TotalRows basetypes.Int64Value `tfsdk:"total_rows"` - state attr.ValueState -} - -func (v PaginationValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - attrTypes := make(map[string]tftypes.Type, 5) - - var val tftypes.Value - var err error - - attrTypes["page"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["size"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["sort"] = basetypes.StringType{}.TerraformType(ctx) - attrTypes["total_pages"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["total_rows"] = basetypes.Int64Type{}.TerraformType(ctx) - - objectType := tftypes.Object{AttributeTypes: attrTypes} - - switch v.state { - case attr.ValueStateKnown: - vals := make(map[string]tftypes.Value, 5) - - val, err = v.Page.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["page"] = val - - val, err = v.Size.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["size"] = val - - val, err = v.Sort.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["sort"] = val - - val, err = v.TotalPages.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["total_pages"] = val - - val, err = v.TotalRows.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["total_rows"] = val - - if err := tftypes.ValidateValue(objectType, vals); err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - return tftypes.NewValue(objectType, vals), nil - case attr.ValueStateNull: - return tftypes.NewValue(objectType, nil), nil - case attr.ValueStateUnknown: - return tftypes.NewValue(objectType, tftypes.UnknownValue), nil - default: - panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) - } -} - -func (v PaginationValue) IsNull() bool { - return v.state == attr.ValueStateNull -} - -func (v PaginationValue) IsUnknown() bool { - return v.state == attr.ValueStateUnknown -} - -func (v PaginationValue) String() string { - return "PaginationValue" -} - -func (v PaginationValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { - var diags diag.Diagnostics - - attributeTypes := map[string]attr.Type{ - "page": basetypes.Int64Type{}, - "size": basetypes.Int64Type{}, - "sort": basetypes.StringType{}, - "total_pages": basetypes.Int64Type{}, - "total_rows": basetypes.Int64Type{}, - } - - if v.IsNull() { - return types.ObjectNull(attributeTypes), diags - } - - if v.IsUnknown() { - return types.ObjectUnknown(attributeTypes), diags - } - - objVal, diags := types.ObjectValue( - attributeTypes, - map[string]attr.Value{ - "page": v.Page, - "size": v.Size, - "sort": v.Sort, - "total_pages": v.TotalPages, - "total_rows": v.TotalRows, - }) - - return objVal, diags -} - -func (v PaginationValue) Equal(o attr.Value) bool { - other, ok := o.(PaginationValue) - - if !ok { - return false - } - - if v.state != other.state { - return false - } - - if v.state != attr.ValueStateKnown { - return true - } - - if !v.Page.Equal(other.Page) { - return false - } - - if !v.Size.Equal(other.Size) { - return false - } - - if !v.Sort.Equal(other.Sort) { - return false - } - - if !v.TotalPages.Equal(other.TotalPages) { - return false - } - - if !v.TotalRows.Equal(other.TotalRows) { - return false - } - - return true -} - -func (v PaginationValue) Type(ctx context.Context) attr.Type { - return PaginationType{ - basetypes.ObjectType{ - AttrTypes: v.AttributeTypes(ctx), - }, - } -} - -func (v PaginationValue) AttributeTypes(ctx context.Context) map[string]attr.Type { - return map[string]attr.Type{ - "page": basetypes.Int64Type{}, - "size": basetypes.Int64Type{}, - "sort": basetypes.StringType{}, - "total_pages": basetypes.Int64Type{}, - "total_rows": basetypes.Int64Type{}, - } -} diff --git a/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go b/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go deleted file mode 100644 index dccae0c4..00000000 --- a/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go +++ /dev/null @@ -1,99 +0,0 @@ -// Code generated by terraform-plugin-framework-generator DO NOT EDIT. - -package sqlserverflexbeta - -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" -) - -func DatabaseResourceSchema(ctx context.Context) schema.Schema { - return schema.Schema{ - Attributes: map[string]schema.Attribute{ - "collation": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - }, - "collation_name": schema.StringAttribute{ - Computed: true, - Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", - }, - "compatibility": schema.Int64Attribute{ - Optional: true, - Computed: true, - Description: "CompatibilityLevel of the Database.", - MarkdownDescription: "CompatibilityLevel of the Database.", - }, - "compatibility_level": schema.Int64Attribute{ - Computed: true, - Description: "CompatibilityLevel of the Database.", - MarkdownDescription: "CompatibilityLevel of the Database.", - }, - "database_name": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The name of the database.", - MarkdownDescription: "The name 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.", - MarkdownDescription: "The name of the database.", - }, - "owner": schema.StringAttribute{ - Required: true, - 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 { - Collation types.String `tfsdk:"collation"` - CollationName types.String `tfsdk:"collation_name"` - Compatibility types.Int64 `tfsdk:"compatibility"` - CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` - DatabaseName types.String `tfsdk:"database_name"` - 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"` -} diff --git a/stackit/internal/services/sqlserverflexbeta/instance/datasources_gen/instance_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/instance/datasources_gen/instance_data_source_gen.go index 87476c3c..f3226581 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/datasources_gen/instance_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/datasources_gen/instance_data_source_gen.go @@ -65,7 +65,7 @@ func InstanceDataSourceSchema(ctx context.Context) schema.Schema { Description: "The id of the instance flavor.", MarkdownDescription: "The id of the instance flavor.", }, - "id": schema.StringAttribute{ + "tf_original_api_id": schema.StringAttribute{ Computed: true, Description: "The ID of the instance.", MarkdownDescription: "The ID of the instance.", @@ -178,7 +178,7 @@ type InstanceModel struct { Edition types.String `tfsdk:"edition"` Encryption EncryptionValue `tfsdk:"encryption"` FlavorId types.String `tfsdk:"flavor_id"` - Id types.String `tfsdk:"id"` + Id types.String `tfsdk:"tf_original_api_id"` InstanceId types.String `tfsdk:"instance_id"` IsDeletable types.Bool `tfsdk:"is_deletable"` Name types.String `tfsdk:"name"` diff --git a/stackit/internal/services/sqlserverflexbeta/instance/resource_test.go b/stackit/internal/services/sqlserverflexbeta/instance/resource_test.go index 64acf850..effced4e 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/resource_test.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/resource_test.go @@ -13,6 +13,7 @@ import ( "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" @@ -160,6 +161,12 @@ func TestAccResourceExample_basic(t *testing.T) { // test create { Config: exBefore, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resName, plancheck.ResourceActionCreate), + plancheck.ExpectNonEmptyPlan(), + }, + }, ConfigStateChecks: []statecheck.StateCheck{ compareValuesSame.AddStateValue( resName, diff --git a/stackit/internal/services/sqlserverflexbeta/user/datasources_gen/user_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/user/datasources_gen/user_data_source_gen.go index 1950c24e..34aef9ca 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/datasources_gen/user_data_source_gen.go +++ b/stackit/internal/services/sqlserverflexbeta/user/datasources_gen/user_data_source_gen.go @@ -98,7 +98,7 @@ func UserDataSourceSchema(ctx context.Context) schema.Schema { "users": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "id": schema.Int64Attribute{ + "tf_original_api_id": schema.Int64Attribute{ Computed: true, Description: "The ID of the user.", MarkdownDescription: "The ID of the user.", -- 2.49.1 From eb736225a9b1634b67d69fe113affb02f143091c Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 9 Feb 2026 15:18:03 +0100 Subject: [PATCH 02/31] feat: refactor builder for datasource --- .../database_data_source_gen.go | 69 + .../database_data_source_gen.go | 81 + .../databases_data_source_gen.go | 1180 ++++++++++ .../resources_gen/database_resource_gen.go | 99 + .../sqlserverflexbeta/flavors/datasource.go | 150 ++ .../flavors_data_source_gen.go | 1909 +++++++++++++++++ .../version_data_source_gen.go | 569 +++++ 7 files changed, 4057 insertions(+) create mode 100644 stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go create mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go create mode 100644 stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go create mode 100644 stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go create mode 100644 stackit/internal/services/sqlserverflexbeta/flavors/datasource.go create mode 100644 stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen/flavors_data_source_gen.go create mode 100644 stackit/internal/services/sqlserverflexbeta/versions/datasources_gen/version_data_source_gen.go diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go new file mode 100644 index 00000000..d5683a6c --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go @@ -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"` +} diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go new file mode 100644 index 00000000..92b1064e --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/database_data_source_gen.go @@ -0,0 +1,81 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package sqlserverflexbeta + +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{ + "collation_name": schema.StringAttribute{ + Computed: true, + Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + }, + "compatibility_level": schema.Int64Attribute{ + Computed: true, + Description: "CompatibilityLevel of the Database.", + MarkdownDescription: "CompatibilityLevel of the Database.", + }, + "database_name": schema.StringAttribute{ + Required: true, + Description: "The name of the database.", + MarkdownDescription: "The name 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 { + CollationName types.String `tfsdk:"collation_name"` + CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` + DatabaseName types.String `tfsdk:"database_name"` + 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"` +} diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go new file mode 100644 index 00000000..71ec8fb4 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/database/datasources_gen/databases_data_source_gen.go @@ -0,0 +1,1180 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package sqlserverflexbeta + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func DatabasesDataSourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "databases": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "created": schema.StringAttribute{ + Computed: true, + Description: "The date when the database was created in RFC3339 format.", + MarkdownDescription: "The date when the database was created in RFC3339 format.", + }, + "id": schema.Int64Attribute{ + Computed: true, + Description: "The id of the database.", + MarkdownDescription: "The id of the database.", + }, + "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.", + }, + }, + CustomType: DatabasesType{ + ObjectType: types.ObjectType{ + AttrTypes: DatabasesValue{}.AttributeTypes(ctx), + }, + }, + }, + Computed: true, + Description: "A list containing all databases for the instance.", + MarkdownDescription: "A list containing all databases for the instance.", + }, + "instance_id": schema.StringAttribute{ + Required: true, + Description: "The ID of the instance.", + MarkdownDescription: "The ID of the instance.", + }, + "page": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Number of the page of items list to be returned.", + MarkdownDescription: "Number of the page of items list to be returned.", + }, + "pagination": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "page": schema.Int64Attribute{ + Computed: true, + }, + "size": schema.Int64Attribute{ + Computed: true, + }, + "sort": schema.StringAttribute{ + Computed: true, + }, + "total_pages": schema.Int64Attribute{ + Computed: true, + }, + "total_rows": schema.Int64Attribute{ + Computed: true, + }, + }, + CustomType: PaginationType{ + ObjectType: types.ObjectType{ + AttrTypes: PaginationValue{}.AttributeTypes(ctx), + }, + }, + Computed: true, + }, + "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", + ), + }, + }, + "size": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Number of items to be returned on each page.", + MarkdownDescription: "Number of items to be returned on each page.", + }, + "sort": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Sorting of the databases to be returned on each page.", + MarkdownDescription: "Sorting of the databases to be returned on each page.", + Validators: []validator.String{ + stringvalidator.OneOf( + "created_at.desc", + "created_at.asc", + "database_id.desc", + "database_id.asc", + "database_name.desc", + "database_name.asc", + "database_owner.desc", + "database_owner.asc", + "index.asc", + "index.desc", + ), + }, + }, + }, + } +} + +type DatabasesModel struct { + Databases types.List `tfsdk:"databases"` + InstanceId types.String `tfsdk:"instance_id"` + Page types.Int64 `tfsdk:"page"` + Pagination PaginationValue `tfsdk:"pagination"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Size types.Int64 `tfsdk:"size"` + Sort types.String `tfsdk:"sort"` +} + +var _ basetypes.ObjectTypable = DatabasesType{} + +type DatabasesType struct { + basetypes.ObjectType +} + +func (t DatabasesType) Equal(o attr.Type) bool { + other, ok := o.(DatabasesType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t DatabasesType) String() string { + return "DatabasesType" +} + +func (t DatabasesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + createdAttribute, ok := attributes["created"] + + if !ok { + diags.AddError( + "Attribute Missing", + `created is missing from object`) + + return nil, diags + } + + createdVal, ok := createdAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`created expected to be basetypes.StringValue, was: %T`, createdAttribute)) + } + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`) + + return nil, diags + } + + idVal, ok := idAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute)) + } + + nameAttribute, ok := attributes["name"] + + if !ok { + diags.AddError( + "Attribute Missing", + `name is missing from object`) + + return nil, diags + } + + nameVal, ok := nameAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute)) + } + + ownerAttribute, ok := attributes["owner"] + + if !ok { + diags.AddError( + "Attribute Missing", + `owner is missing from object`) + + return nil, diags + } + + ownerVal, ok := ownerAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return DatabasesValue{ + Created: createdVal, + Id: idVal, + Name: nameVal, + Owner: ownerVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewDatabasesValueNull() DatabasesValue { + return DatabasesValue{ + state: attr.ValueStateNull, + } +} + +func NewDatabasesValueUnknown() DatabasesValue { + return DatabasesValue{ + state: attr.ValueStateUnknown, + } +} + +func NewDatabasesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (DatabasesValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing DatabasesValue Attribute Value", + "While creating a DatabasesValue value, a missing attribute value was detected. "+ + "A DatabasesValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("DatabasesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid DatabasesValue Attribute Type", + "While creating a DatabasesValue value, an invalid attribute value was detected. "+ + "A DatabasesValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("DatabasesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("DatabasesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra DatabasesValue Attribute Value", + "While creating a DatabasesValue value, an extra attribute value was detected. "+ + "A DatabasesValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra DatabasesValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewDatabasesValueUnknown(), diags + } + + createdAttribute, ok := attributes["created"] + + if !ok { + diags.AddError( + "Attribute Missing", + `created is missing from object`) + + return NewDatabasesValueUnknown(), diags + } + + createdVal, ok := createdAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`created expected to be basetypes.StringValue, was: %T`, createdAttribute)) + } + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`) + + return NewDatabasesValueUnknown(), diags + } + + idVal, ok := idAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute)) + } + + nameAttribute, ok := attributes["name"] + + if !ok { + diags.AddError( + "Attribute Missing", + `name is missing from object`) + + return NewDatabasesValueUnknown(), diags + } + + nameVal, ok := nameAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute)) + } + + ownerAttribute, ok := attributes["owner"] + + if !ok { + diags.AddError( + "Attribute Missing", + `owner is missing from object`) + + return NewDatabasesValueUnknown(), diags + } + + ownerVal, ok := ownerAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute)) + } + + if diags.HasError() { + return NewDatabasesValueUnknown(), diags + } + + return DatabasesValue{ + Created: createdVal, + Id: idVal, + Name: nameVal, + Owner: ownerVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewDatabasesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) DatabasesValue { + object, diags := NewDatabasesValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewDatabasesValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t DatabasesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewDatabasesValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewDatabasesValueUnknown(), nil + } + + if in.IsNull() { + return NewDatabasesValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewDatabasesValueMust(DatabasesValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t DatabasesType) ValueType(ctx context.Context) attr.Value { + return DatabasesValue{} +} + +var _ basetypes.ObjectValuable = DatabasesValue{} + +type DatabasesValue struct { + Created basetypes.StringValue `tfsdk:"created"` + Id basetypes.Int64Value `tfsdk:"id"` + Name basetypes.StringValue `tfsdk:"name"` + Owner basetypes.StringValue `tfsdk:"owner"` + state attr.ValueState +} + +func (v DatabasesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 4) + + var val tftypes.Value + var err error + + attrTypes["created"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["id"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["owner"] = basetypes.StringType{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 4) + + val, err = v.Created.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["created"] = val + + val, err = v.Id.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["id"] = val + + val, err = v.Name.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["name"] = val + + val, err = v.Owner.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["owner"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v DatabasesValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v DatabasesValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v DatabasesValue) String() string { + return "DatabasesValue" +} + +func (v DatabasesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "created": basetypes.StringType{}, + "id": basetypes.Int64Type{}, + "name": basetypes.StringType{}, + "owner": basetypes.StringType{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "created": v.Created, + "id": v.Id, + "name": v.Name, + "owner": v.Owner, + }) + + return objVal, diags +} + +func (v DatabasesValue) Equal(o attr.Value) bool { + other, ok := o.(DatabasesValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Created.Equal(other.Created) { + return false + } + + if !v.Id.Equal(other.Id) { + return false + } + + if !v.Name.Equal(other.Name) { + return false + } + + if !v.Owner.Equal(other.Owner) { + return false + } + + return true +} + +func (v DatabasesValue) Type(ctx context.Context) attr.Type { + return DatabasesType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v DatabasesValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "created": basetypes.StringType{}, + "id": basetypes.Int64Type{}, + "name": basetypes.StringType{}, + "owner": basetypes.StringType{}, + } +} + +var _ basetypes.ObjectTypable = PaginationType{} + +type PaginationType struct { + basetypes.ObjectType +} + +func (t PaginationType) Equal(o attr.Type) bool { + other, ok := o.(PaginationType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t PaginationType) String() string { + return "PaginationType" +} + +func (t PaginationType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + pageAttribute, ok := attributes["page"] + + if !ok { + diags.AddError( + "Attribute Missing", + `page is missing from object`) + + return nil, diags + } + + pageVal, ok := pageAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) + } + + sizeAttribute, ok := attributes["size"] + + if !ok { + diags.AddError( + "Attribute Missing", + `size is missing from object`) + + return nil, diags + } + + sizeVal, ok := sizeAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) + } + + sortAttribute, ok := attributes["sort"] + + if !ok { + diags.AddError( + "Attribute Missing", + `sort is missing from object`) + + return nil, diags + } + + sortVal, ok := sortAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) + } + + totalPagesAttribute, ok := attributes["total_pages"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_pages is missing from object`) + + return nil, diags + } + + totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) + } + + totalRowsAttribute, ok := attributes["total_rows"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_rows is missing from object`) + + return nil, diags + } + + totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return PaginationValue{ + Page: pageVal, + Size: sizeVal, + Sort: sortVal, + TotalPages: totalPagesVal, + TotalRows: totalRowsVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewPaginationValueNull() PaginationValue { + return PaginationValue{ + state: attr.ValueStateNull, + } +} + +func NewPaginationValueUnknown() PaginationValue { + return PaginationValue{ + state: attr.ValueStateUnknown, + } +} + +func NewPaginationValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (PaginationValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing PaginationValue Attribute Value", + "While creating a PaginationValue value, a missing attribute value was detected. "+ + "A PaginationValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid PaginationValue Attribute Type", + "While creating a PaginationValue value, an invalid attribute value was detected. "+ + "A PaginationValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra PaginationValue Attribute Value", + "While creating a PaginationValue value, an extra attribute value was detected. "+ + "A PaginationValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra PaginationValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewPaginationValueUnknown(), diags + } + + pageAttribute, ok := attributes["page"] + + if !ok { + diags.AddError( + "Attribute Missing", + `page is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + pageVal, ok := pageAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) + } + + sizeAttribute, ok := attributes["size"] + + if !ok { + diags.AddError( + "Attribute Missing", + `size is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + sizeVal, ok := sizeAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) + } + + sortAttribute, ok := attributes["sort"] + + if !ok { + diags.AddError( + "Attribute Missing", + `sort is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + sortVal, ok := sortAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) + } + + totalPagesAttribute, ok := attributes["total_pages"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_pages is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) + } + + totalRowsAttribute, ok := attributes["total_rows"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_rows is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) + } + + if diags.HasError() { + return NewPaginationValueUnknown(), diags + } + + return PaginationValue{ + Page: pageVal, + Size: sizeVal, + Sort: sortVal, + TotalPages: totalPagesVal, + TotalRows: totalRowsVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewPaginationValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) PaginationValue { + object, diags := NewPaginationValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewPaginationValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t PaginationType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewPaginationValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewPaginationValueUnknown(), nil + } + + if in.IsNull() { + return NewPaginationValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewPaginationValueMust(PaginationValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t PaginationType) ValueType(ctx context.Context) attr.Value { + return PaginationValue{} +} + +var _ basetypes.ObjectValuable = PaginationValue{} + +type PaginationValue struct { + Page basetypes.Int64Value `tfsdk:"page"` + Size basetypes.Int64Value `tfsdk:"size"` + Sort basetypes.StringValue `tfsdk:"sort"` + TotalPages basetypes.Int64Value `tfsdk:"total_pages"` + TotalRows basetypes.Int64Value `tfsdk:"total_rows"` + state attr.ValueState +} + +func (v PaginationValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 5) + + var val tftypes.Value + var err error + + attrTypes["page"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["size"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["sort"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["total_pages"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["total_rows"] = basetypes.Int64Type{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 5) + + val, err = v.Page.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["page"] = val + + val, err = v.Size.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["size"] = val + + val, err = v.Sort.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["sort"] = val + + val, err = v.TotalPages.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["total_pages"] = val + + val, err = v.TotalRows.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["total_rows"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v PaginationValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v PaginationValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v PaginationValue) String() string { + return "PaginationValue" +} + +func (v PaginationValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "page": basetypes.Int64Type{}, + "size": basetypes.Int64Type{}, + "sort": basetypes.StringType{}, + "total_pages": basetypes.Int64Type{}, + "total_rows": basetypes.Int64Type{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "page": v.Page, + "size": v.Size, + "sort": v.Sort, + "total_pages": v.TotalPages, + "total_rows": v.TotalRows, + }) + + return objVal, diags +} + +func (v PaginationValue) Equal(o attr.Value) bool { + other, ok := o.(PaginationValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Page.Equal(other.Page) { + return false + } + + if !v.Size.Equal(other.Size) { + return false + } + + if !v.Sort.Equal(other.Sort) { + return false + } + + if !v.TotalPages.Equal(other.TotalPages) { + return false + } + + if !v.TotalRows.Equal(other.TotalRows) { + return false + } + + return true +} + +func (v PaginationValue) Type(ctx context.Context) attr.Type { + return PaginationType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v PaginationValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "page": basetypes.Int64Type{}, + "size": basetypes.Int64Type{}, + "sort": basetypes.StringType{}, + "total_pages": basetypes.Int64Type{}, + "total_rows": basetypes.Int64Type{}, + } +} diff --git a/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go b/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go new file mode 100644 index 00000000..dccae0c4 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/database/resources_gen/database_resource_gen.go @@ -0,0 +1,99 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package sqlserverflexbeta + +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" +) + +func DatabaseResourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "collation": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + }, + "collation_name": schema.StringAttribute{ + Computed: true, + Description: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + MarkdownDescription: "The collation of the database. This database collation should match the *collation_name* of one of the collations given by the **Get database collation list** endpoint.", + }, + "compatibility": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "CompatibilityLevel of the Database.", + MarkdownDescription: "CompatibilityLevel of the Database.", + }, + "compatibility_level": schema.Int64Attribute{ + Computed: true, + Description: "CompatibilityLevel of the Database.", + MarkdownDescription: "CompatibilityLevel of the Database.", + }, + "database_name": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The name of the database.", + MarkdownDescription: "The name 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.", + MarkdownDescription: "The name of the database.", + }, + "owner": schema.StringAttribute{ + Required: true, + 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 { + Collation types.String `tfsdk:"collation"` + CollationName types.String `tfsdk:"collation_name"` + Compatibility types.Int64 `tfsdk:"compatibility"` + CompatibilityLevel types.Int64 `tfsdk:"compatibility_level"` + DatabaseName types.String `tfsdk:"database_name"` + 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"` +} diff --git a/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go b/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go new file mode 100644 index 00000000..9492387d --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go @@ -0,0 +1,150 @@ +package sqlserverflexbeta + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + "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" + + sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" + + sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen" +) + +var _ datasource.DataSource = (*flavorsDataSource)(nil) + +const errorPrefix = "[Sqlserverflexbeta - Flavors]" + +func NewFlavorsDataSource() datasource.DataSource { + return &flavorsDataSource{} +} + +type dsModel struct { + sqlserverflexbetaGen.FlavorsModel + TfId types.String `tfsdk:"id"` +} + +type flavorsDataSource struct{ + client *sqlserverflexbetaPkg.APIClient + providerData core.ProviderData +} + +func (d *flavorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_flavors" +} + +func (d *flavorsDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = sqlserverflexbetaGen.FlavorsDataSourceSchema(ctx) + resp.Schema.Attributes["id"] = schema.StringAttribute{ + Computed: true, + Description: "The terraform internal identifier.", + MarkdownDescription: "The terraform internal identifier.", + } +} + +// Configure adds the provider configured client to the data source. +func (d *flavorsDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(d.providerData.RoundTripper), + utils.UserAgentConfigOption(d.providerData.Version), + } + if d.providerData.SqlserverflexbetaCustomEndpoint != "" { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(d.providerData.SqlserverflexbetaCustomEndpoint), + ) + } else { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithRegion(d.providerData.GetRegion()), + ) + } + apiClient, err := sqlserverflexbetaPkg.NewAPIClient(apiClientConfigOptions...) + if err != nil { + resp.Diagnostics.AddError( + "Error configuring API client", + fmt.Sprintf( + "Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", + err, + ), + ) + return + } + d.client = apiClient + tflog.Info(ctx, fmt.Sprintf("%s client configured", errorPrefix)) +} + +func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data dsModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := data.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(data.Region) + flavorsId := data.FlavorsId.ValueString() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + // TODO: implement needed fields + ctx = tflog.SetField(ctx, "flavors_id", flavorsId) + + // TODO: refactor to correct implementation + flavorsResp, err := d.client.GetFlavorsRequest(ctx, projectId, region, flavorsId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading flavors", + fmt.Sprintf("flavors with ID %q does not exist in project %q.", flavorsId, projectId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + ctx = core.LogResponse(ctx) + + + data.TfId = utils.BuildInternalTerraformId(projectId, region, ..) + + // TODO: fill remaining fields + // data.Flavors = types.Sometype(apiResponse.GetFlavors()) + // data.Page = types.Sometype(apiResponse.GetPage()) + // data.Pagination = types.Sometype(apiResponse.GetPagination()) + // data.ProjectId = types.Sometype(apiResponse.GetProjectId()) + // data.Region = types.Sometype(apiResponse.GetRegion()) + // data.Size = types.Sometype(apiResponse.GetSize()) + // data.Sort = types.Sometype(apiResponse.GetSort())// Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + tflog.Info(ctx, fmt.Sprintf("%s read successful", errorPrefix)) +} diff --git a/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen/flavors_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen/flavors_data_source_gen.go new file mode 100644 index 00000000..a9d35ba1 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen/flavors_data_source_gen.go @@ -0,0 +1,1909 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package sqlserverflexbeta + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func FlavorsDataSourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "flavors": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "cpu": schema.Int64Attribute{ + Computed: true, + Description: "The cpu count of the instance.", + MarkdownDescription: "The cpu count of the instance.", + }, + "description": schema.StringAttribute{ + Computed: true, + Description: "The flavor description.", + MarkdownDescription: "The flavor description.", + }, + "tf_original_api_id": schema.StringAttribute{ + Computed: true, + Description: "The id of the instance flavor.", + MarkdownDescription: "The id of the instance flavor.", + }, + "max_gb": schema.Int64Attribute{ + Computed: true, + Description: "maximum storage which can be ordered for the flavor in Gigabyte.", + MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.", + }, + "memory": schema.Int64Attribute{ + Computed: true, + Description: "The memory of the instance in Gibibyte.", + MarkdownDescription: "The memory of the instance in Gibibyte.", + }, + "min_gb": schema.Int64Attribute{ + Computed: true, + Description: "minimum storage which is required to order in Gigabyte.", + MarkdownDescription: "minimum storage which is required to order in Gigabyte.", + }, + "node_type": schema.StringAttribute{ + Computed: true, + Description: "defines the nodeType it can be either single or HA", + MarkdownDescription: "defines the nodeType it can be either single or HA", + }, + "storage_classes": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "class": schema.StringAttribute{ + Computed: true, + }, + "max_io_per_sec": schema.Int64Attribute{ + Computed: true, + }, + "max_through_in_mb": schema.Int64Attribute{ + Computed: true, + }, + }, + CustomType: StorageClassesType{ + ObjectType: types.ObjectType{ + AttrTypes: StorageClassesValue{}.AttributeTypes(ctx), + }, + }, + }, + Computed: true, + Description: "maximum storage which can be ordered for the flavor in Gigabyte.", + MarkdownDescription: "maximum storage which can be ordered for the flavor in Gigabyte.", + }, + }, + CustomType: FlavorsType{ + ObjectType: types.ObjectType{ + AttrTypes: FlavorsValue{}.AttributeTypes(ctx), + }, + }, + }, + Computed: true, + Description: "List of flavors available for the project.", + MarkdownDescription: "List of flavors available for the project.", + }, + "page": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Number of the page of items list to be returned.", + MarkdownDescription: "Number of the page of items list to be returned.", + }, + "pagination": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "page": schema.Int64Attribute{ + Computed: true, + }, + "size": schema.Int64Attribute{ + Computed: true, + }, + "sort": schema.StringAttribute{ + Computed: true, + }, + "total_pages": schema.Int64Attribute{ + Computed: true, + }, + "total_rows": schema.Int64Attribute{ + Computed: true, + }, + }, + CustomType: PaginationType{ + ObjectType: types.ObjectType{ + AttrTypes: PaginationValue{}.AttributeTypes(ctx), + }, + }, + Computed: true, + }, + "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", + ), + }, + }, + "size": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Number of items to be returned on each page.", + MarkdownDescription: "Number of items to be returned on each page.", + }, + "sort": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Sorting of the flavors to be returned on each page.", + MarkdownDescription: "Sorting of the flavors to be returned on each page.", + Validators: []validator.String{ + stringvalidator.OneOf( + "index.desc", + "index.asc", + "cpu.desc", + "cpu.asc", + "flavor_description.asc", + "flavor_description.desc", + "id.desc", + "id.asc", + "size_max.desc", + "size_max.asc", + "ram.desc", + "ram.asc", + "size_min.desc", + "size_min.asc", + "storage_class.asc", + "storage_class.desc", + "node_type.asc", + "node_type.desc", + ), + }, + }, + }, + } +} + +type FlavorsModel struct { + Flavors types.List `tfsdk:"flavors"` + Page types.Int64 `tfsdk:"page"` + Pagination PaginationValue `tfsdk:"pagination"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Size types.Int64 `tfsdk:"size"` + Sort types.String `tfsdk:"sort"` +} + +var _ basetypes.ObjectTypable = FlavorsType{} + +type FlavorsType struct { + basetypes.ObjectType +} + +func (t FlavorsType) Equal(o attr.Type) bool { + other, ok := o.(FlavorsType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t FlavorsType) String() string { + return "FlavorsType" +} + +func (t FlavorsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + cpuAttribute, ok := attributes["cpu"] + + if !ok { + diags.AddError( + "Attribute Missing", + `cpu is missing from object`) + + return nil, diags + } + + cpuVal, ok := cpuAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`cpu expected to be basetypes.Int64Value, was: %T`, cpuAttribute)) + } + + descriptionAttribute, ok := attributes["description"] + + if !ok { + diags.AddError( + "Attribute Missing", + `description is missing from object`) + + return nil, diags + } + + descriptionVal, ok := descriptionAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute)) + } + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`) + + return nil, diags + } + + idVal, ok := idAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute)) + } + + maxGbAttribute, ok := attributes["max_gb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_gb is missing from object`) + + return nil, diags + } + + maxGbVal, ok := maxGbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_gb expected to be basetypes.Int64Value, was: %T`, maxGbAttribute)) + } + + memoryAttribute, ok := attributes["memory"] + + if !ok { + diags.AddError( + "Attribute Missing", + `memory is missing from object`) + + return nil, diags + } + + memoryVal, ok := memoryAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`memory expected to be basetypes.Int64Value, was: %T`, memoryAttribute)) + } + + minGbAttribute, ok := attributes["min_gb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `min_gb is missing from object`) + + return nil, diags + } + + minGbVal, ok := minGbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`min_gb expected to be basetypes.Int64Value, was: %T`, minGbAttribute)) + } + + nodeTypeAttribute, ok := attributes["node_type"] + + if !ok { + diags.AddError( + "Attribute Missing", + `node_type is missing from object`) + + return nil, diags + } + + nodeTypeVal, ok := nodeTypeAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`node_type expected to be basetypes.StringValue, was: %T`, nodeTypeAttribute)) + } + + storageClassesAttribute, ok := attributes["storage_classes"] + + if !ok { + diags.AddError( + "Attribute Missing", + `storage_classes is missing from object`) + + return nil, diags + } + + storageClassesVal, ok := storageClassesAttribute.(basetypes.ListValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`storage_classes expected to be basetypes.ListValue, was: %T`, storageClassesAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return FlavorsValue{ + Cpu: cpuVal, + Description: descriptionVal, + Id: idVal, + MaxGb: maxGbVal, + Memory: memoryVal, + MinGb: minGbVal, + NodeType: nodeTypeVal, + StorageClasses: storageClassesVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewFlavorsValueNull() FlavorsValue { + return FlavorsValue{ + state: attr.ValueStateNull, + } +} + +func NewFlavorsValueUnknown() FlavorsValue { + return FlavorsValue{ + state: attr.ValueStateUnknown, + } +} + +func NewFlavorsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (FlavorsValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing FlavorsValue Attribute Value", + "While creating a FlavorsValue value, a missing attribute value was detected. "+ + "A FlavorsValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("FlavorsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid FlavorsValue Attribute Type", + "While creating a FlavorsValue value, an invalid attribute value was detected. "+ + "A FlavorsValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("FlavorsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("FlavorsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra FlavorsValue Attribute Value", + "While creating a FlavorsValue value, an extra attribute value was detected. "+ + "A FlavorsValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra FlavorsValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewFlavorsValueUnknown(), diags + } + + cpuAttribute, ok := attributes["cpu"] + + if !ok { + diags.AddError( + "Attribute Missing", + `cpu is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + cpuVal, ok := cpuAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`cpu expected to be basetypes.Int64Value, was: %T`, cpuAttribute)) + } + + descriptionAttribute, ok := attributes["description"] + + if !ok { + diags.AddError( + "Attribute Missing", + `description is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + descriptionVal, ok := descriptionAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute)) + } + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + idVal, ok := idAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute)) + } + + maxGbAttribute, ok := attributes["max_gb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_gb is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + maxGbVal, ok := maxGbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_gb expected to be basetypes.Int64Value, was: %T`, maxGbAttribute)) + } + + memoryAttribute, ok := attributes["memory"] + + if !ok { + diags.AddError( + "Attribute Missing", + `memory is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + memoryVal, ok := memoryAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`memory expected to be basetypes.Int64Value, was: %T`, memoryAttribute)) + } + + minGbAttribute, ok := attributes["min_gb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `min_gb is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + minGbVal, ok := minGbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`min_gb expected to be basetypes.Int64Value, was: %T`, minGbAttribute)) + } + + nodeTypeAttribute, ok := attributes["node_type"] + + if !ok { + diags.AddError( + "Attribute Missing", + `node_type is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + nodeTypeVal, ok := nodeTypeAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`node_type expected to be basetypes.StringValue, was: %T`, nodeTypeAttribute)) + } + + storageClassesAttribute, ok := attributes["storage_classes"] + + if !ok { + diags.AddError( + "Attribute Missing", + `storage_classes is missing from object`) + + return NewFlavorsValueUnknown(), diags + } + + storageClassesVal, ok := storageClassesAttribute.(basetypes.ListValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`storage_classes expected to be basetypes.ListValue, was: %T`, storageClassesAttribute)) + } + + if diags.HasError() { + return NewFlavorsValueUnknown(), diags + } + + return FlavorsValue{ + Cpu: cpuVal, + Description: descriptionVal, + Id: idVal, + MaxGb: maxGbVal, + Memory: memoryVal, + MinGb: minGbVal, + NodeType: nodeTypeVal, + StorageClasses: storageClassesVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewFlavorsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) FlavorsValue { + object, diags := NewFlavorsValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewFlavorsValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t FlavorsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewFlavorsValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewFlavorsValueUnknown(), nil + } + + if in.IsNull() { + return NewFlavorsValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewFlavorsValueMust(FlavorsValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t FlavorsType) ValueType(ctx context.Context) attr.Value { + return FlavorsValue{} +} + +var _ basetypes.ObjectValuable = FlavorsValue{} + +type FlavorsValue struct { + Cpu basetypes.Int64Value `tfsdk:"cpu"` + Description basetypes.StringValue `tfsdk:"description"` + Id basetypes.StringValue `tfsdk:"id"` + MaxGb basetypes.Int64Value `tfsdk:"max_gb"` + Memory basetypes.Int64Value `tfsdk:"memory"` + MinGb basetypes.Int64Value `tfsdk:"min_gb"` + NodeType basetypes.StringValue `tfsdk:"node_type"` + StorageClasses basetypes.ListValue `tfsdk:"storage_classes"` + state attr.ValueState +} + +func (v FlavorsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 8) + + var val tftypes.Value + var err error + + attrTypes["cpu"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["description"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["max_gb"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["memory"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["min_gb"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["node_type"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["storage_classes"] = basetypes.ListType{ + ElemType: StorageClassesValue{}.Type(ctx), + }.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 8) + + val, err = v.Cpu.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["cpu"] = val + + val, err = v.Description.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["description"] = val + + val, err = v.Id.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["id"] = val + + val, err = v.MaxGb.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["max_gb"] = val + + val, err = v.Memory.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["memory"] = val + + val, err = v.MinGb.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["min_gb"] = val + + val, err = v.NodeType.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["node_type"] = val + + val, err = v.StorageClasses.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["storage_classes"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v FlavorsValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v FlavorsValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v FlavorsValue) String() string { + return "FlavorsValue" +} + +func (v FlavorsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + storageClasses := types.ListValueMust( + StorageClassesType{ + basetypes.ObjectType{ + AttrTypes: StorageClassesValue{}.AttributeTypes(ctx), + }, + }, + v.StorageClasses.Elements(), + ) + + if v.StorageClasses.IsNull() { + storageClasses = types.ListNull( + StorageClassesType{ + basetypes.ObjectType{ + AttrTypes: StorageClassesValue{}.AttributeTypes(ctx), + }, + }, + ) + } + + if v.StorageClasses.IsUnknown() { + storageClasses = types.ListUnknown( + StorageClassesType{ + basetypes.ObjectType{ + AttrTypes: StorageClassesValue{}.AttributeTypes(ctx), + }, + }, + ) + } + + attributeTypes := map[string]attr.Type{ + "cpu": basetypes.Int64Type{}, + "description": basetypes.StringType{}, + "id": basetypes.StringType{}, + "max_gb": basetypes.Int64Type{}, + "memory": basetypes.Int64Type{}, + "min_gb": basetypes.Int64Type{}, + "node_type": basetypes.StringType{}, + "storage_classes": basetypes.ListType{ + ElemType: StorageClassesValue{}.Type(ctx), + }, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "cpu": v.Cpu, + "description": v.Description, + "id": v.Id, + "max_gb": v.MaxGb, + "memory": v.Memory, + "min_gb": v.MinGb, + "node_type": v.NodeType, + "storage_classes": storageClasses, + }) + + return objVal, diags +} + +func (v FlavorsValue) Equal(o attr.Value) bool { + other, ok := o.(FlavorsValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Cpu.Equal(other.Cpu) { + return false + } + + if !v.Description.Equal(other.Description) { + return false + } + + if !v.Id.Equal(other.Id) { + return false + } + + if !v.MaxGb.Equal(other.MaxGb) { + return false + } + + if !v.Memory.Equal(other.Memory) { + return false + } + + if !v.MinGb.Equal(other.MinGb) { + return false + } + + if !v.NodeType.Equal(other.NodeType) { + return false + } + + if !v.StorageClasses.Equal(other.StorageClasses) { + return false + } + + return true +} + +func (v FlavorsValue) Type(ctx context.Context) attr.Type { + return FlavorsType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v FlavorsValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "cpu": basetypes.Int64Type{}, + "description": basetypes.StringType{}, + "id": basetypes.StringType{}, + "max_gb": basetypes.Int64Type{}, + "memory": basetypes.Int64Type{}, + "min_gb": basetypes.Int64Type{}, + "node_type": basetypes.StringType{}, + "storage_classes": basetypes.ListType{ + ElemType: StorageClassesValue{}.Type(ctx), + }, + } +} + +var _ basetypes.ObjectTypable = StorageClassesType{} + +type StorageClassesType struct { + basetypes.ObjectType +} + +func (t StorageClassesType) Equal(o attr.Type) bool { + other, ok := o.(StorageClassesType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t StorageClassesType) String() string { + return "StorageClassesType" +} + +func (t StorageClassesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + classAttribute, ok := attributes["class"] + + if !ok { + diags.AddError( + "Attribute Missing", + `class is missing from object`) + + return nil, diags + } + + classVal, ok := classAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`class expected to be basetypes.StringValue, was: %T`, classAttribute)) + } + + maxIoPerSecAttribute, ok := attributes["max_io_per_sec"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_io_per_sec is missing from object`) + + return nil, diags + } + + maxIoPerSecVal, ok := maxIoPerSecAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_io_per_sec expected to be basetypes.Int64Value, was: %T`, maxIoPerSecAttribute)) + } + + maxThroughInMbAttribute, ok := attributes["max_through_in_mb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_through_in_mb is missing from object`) + + return nil, diags + } + + maxThroughInMbVal, ok := maxThroughInMbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_through_in_mb expected to be basetypes.Int64Value, was: %T`, maxThroughInMbAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return StorageClassesValue{ + Class: classVal, + MaxIoPerSec: maxIoPerSecVal, + MaxThroughInMb: maxThroughInMbVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewStorageClassesValueNull() StorageClassesValue { + return StorageClassesValue{ + state: attr.ValueStateNull, + } +} + +func NewStorageClassesValueUnknown() StorageClassesValue { + return StorageClassesValue{ + state: attr.ValueStateUnknown, + } +} + +func NewStorageClassesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (StorageClassesValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing StorageClassesValue Attribute Value", + "While creating a StorageClassesValue value, a missing attribute value was detected. "+ + "A StorageClassesValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("StorageClassesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid StorageClassesValue Attribute Type", + "While creating a StorageClassesValue value, an invalid attribute value was detected. "+ + "A StorageClassesValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("StorageClassesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("StorageClassesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra StorageClassesValue Attribute Value", + "While creating a StorageClassesValue value, an extra attribute value was detected. "+ + "A StorageClassesValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra StorageClassesValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewStorageClassesValueUnknown(), diags + } + + classAttribute, ok := attributes["class"] + + if !ok { + diags.AddError( + "Attribute Missing", + `class is missing from object`) + + return NewStorageClassesValueUnknown(), diags + } + + classVal, ok := classAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`class expected to be basetypes.StringValue, was: %T`, classAttribute)) + } + + maxIoPerSecAttribute, ok := attributes["max_io_per_sec"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_io_per_sec is missing from object`) + + return NewStorageClassesValueUnknown(), diags + } + + maxIoPerSecVal, ok := maxIoPerSecAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_io_per_sec expected to be basetypes.Int64Value, was: %T`, maxIoPerSecAttribute)) + } + + maxThroughInMbAttribute, ok := attributes["max_through_in_mb"] + + if !ok { + diags.AddError( + "Attribute Missing", + `max_through_in_mb is missing from object`) + + return NewStorageClassesValueUnknown(), diags + } + + maxThroughInMbVal, ok := maxThroughInMbAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`max_through_in_mb expected to be basetypes.Int64Value, was: %T`, maxThroughInMbAttribute)) + } + + if diags.HasError() { + return NewStorageClassesValueUnknown(), diags + } + + return StorageClassesValue{ + Class: classVal, + MaxIoPerSec: maxIoPerSecVal, + MaxThroughInMb: maxThroughInMbVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewStorageClassesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) StorageClassesValue { + object, diags := NewStorageClassesValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewStorageClassesValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t StorageClassesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewStorageClassesValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewStorageClassesValueUnknown(), nil + } + + if in.IsNull() { + return NewStorageClassesValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewStorageClassesValueMust(StorageClassesValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t StorageClassesType) ValueType(ctx context.Context) attr.Value { + return StorageClassesValue{} +} + +var _ basetypes.ObjectValuable = StorageClassesValue{} + +type StorageClassesValue struct { + Class basetypes.StringValue `tfsdk:"class"` + MaxIoPerSec basetypes.Int64Value `tfsdk:"max_io_per_sec"` + MaxThroughInMb basetypes.Int64Value `tfsdk:"max_through_in_mb"` + state attr.ValueState +} + +func (v StorageClassesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 3) + + var val tftypes.Value + var err error + + attrTypes["class"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["max_io_per_sec"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["max_through_in_mb"] = basetypes.Int64Type{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 3) + + val, err = v.Class.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["class"] = val + + val, err = v.MaxIoPerSec.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["max_io_per_sec"] = val + + val, err = v.MaxThroughInMb.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["max_through_in_mb"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v StorageClassesValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v StorageClassesValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v StorageClassesValue) String() string { + return "StorageClassesValue" +} + +func (v StorageClassesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "class": basetypes.StringType{}, + "max_io_per_sec": basetypes.Int64Type{}, + "max_through_in_mb": basetypes.Int64Type{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "class": v.Class, + "max_io_per_sec": v.MaxIoPerSec, + "max_through_in_mb": v.MaxThroughInMb, + }) + + return objVal, diags +} + +func (v StorageClassesValue) Equal(o attr.Value) bool { + other, ok := o.(StorageClassesValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Class.Equal(other.Class) { + return false + } + + if !v.MaxIoPerSec.Equal(other.MaxIoPerSec) { + return false + } + + if !v.MaxThroughInMb.Equal(other.MaxThroughInMb) { + return false + } + + return true +} + +func (v StorageClassesValue) Type(ctx context.Context) attr.Type { + return StorageClassesType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v StorageClassesValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "class": basetypes.StringType{}, + "max_io_per_sec": basetypes.Int64Type{}, + "max_through_in_mb": basetypes.Int64Type{}, + } +} + +var _ basetypes.ObjectTypable = PaginationType{} + +type PaginationType struct { + basetypes.ObjectType +} + +func (t PaginationType) Equal(o attr.Type) bool { + other, ok := o.(PaginationType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t PaginationType) String() string { + return "PaginationType" +} + +func (t PaginationType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + pageAttribute, ok := attributes["page"] + + if !ok { + diags.AddError( + "Attribute Missing", + `page is missing from object`) + + return nil, diags + } + + pageVal, ok := pageAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) + } + + sizeAttribute, ok := attributes["size"] + + if !ok { + diags.AddError( + "Attribute Missing", + `size is missing from object`) + + return nil, diags + } + + sizeVal, ok := sizeAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) + } + + sortAttribute, ok := attributes["sort"] + + if !ok { + diags.AddError( + "Attribute Missing", + `sort is missing from object`) + + return nil, diags + } + + sortVal, ok := sortAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) + } + + totalPagesAttribute, ok := attributes["total_pages"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_pages is missing from object`) + + return nil, diags + } + + totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) + } + + totalRowsAttribute, ok := attributes["total_rows"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_rows is missing from object`) + + return nil, diags + } + + totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return PaginationValue{ + Page: pageVal, + Size: sizeVal, + Sort: sortVal, + TotalPages: totalPagesVal, + TotalRows: totalRowsVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewPaginationValueNull() PaginationValue { + return PaginationValue{ + state: attr.ValueStateNull, + } +} + +func NewPaginationValueUnknown() PaginationValue { + return PaginationValue{ + state: attr.ValueStateUnknown, + } +} + +func NewPaginationValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (PaginationValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing PaginationValue Attribute Value", + "While creating a PaginationValue value, a missing attribute value was detected. "+ + "A PaginationValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid PaginationValue Attribute Type", + "While creating a PaginationValue value, an invalid attribute value was detected. "+ + "A PaginationValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("PaginationValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra PaginationValue Attribute Value", + "While creating a PaginationValue value, an extra attribute value was detected. "+ + "A PaginationValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra PaginationValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewPaginationValueUnknown(), diags + } + + pageAttribute, ok := attributes["page"] + + if !ok { + diags.AddError( + "Attribute Missing", + `page is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + pageVal, ok := pageAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`page expected to be basetypes.Int64Value, was: %T`, pageAttribute)) + } + + sizeAttribute, ok := attributes["size"] + + if !ok { + diags.AddError( + "Attribute Missing", + `size is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + sizeVal, ok := sizeAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`size expected to be basetypes.Int64Value, was: %T`, sizeAttribute)) + } + + sortAttribute, ok := attributes["sort"] + + if !ok { + diags.AddError( + "Attribute Missing", + `sort is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + sortVal, ok := sortAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`sort expected to be basetypes.StringValue, was: %T`, sortAttribute)) + } + + totalPagesAttribute, ok := attributes["total_pages"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_pages is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + totalPagesVal, ok := totalPagesAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_pages expected to be basetypes.Int64Value, was: %T`, totalPagesAttribute)) + } + + totalRowsAttribute, ok := attributes["total_rows"] + + if !ok { + diags.AddError( + "Attribute Missing", + `total_rows is missing from object`) + + return NewPaginationValueUnknown(), diags + } + + totalRowsVal, ok := totalRowsAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`total_rows expected to be basetypes.Int64Value, was: %T`, totalRowsAttribute)) + } + + if diags.HasError() { + return NewPaginationValueUnknown(), diags + } + + return PaginationValue{ + Page: pageVal, + Size: sizeVal, + Sort: sortVal, + TotalPages: totalPagesVal, + TotalRows: totalRowsVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewPaginationValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) PaginationValue { + object, diags := NewPaginationValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewPaginationValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t PaginationType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewPaginationValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewPaginationValueUnknown(), nil + } + + if in.IsNull() { + return NewPaginationValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewPaginationValueMust(PaginationValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t PaginationType) ValueType(ctx context.Context) attr.Value { + return PaginationValue{} +} + +var _ basetypes.ObjectValuable = PaginationValue{} + +type PaginationValue struct { + Page basetypes.Int64Value `tfsdk:"page"` + Size basetypes.Int64Value `tfsdk:"size"` + Sort basetypes.StringValue `tfsdk:"sort"` + TotalPages basetypes.Int64Value `tfsdk:"total_pages"` + TotalRows basetypes.Int64Value `tfsdk:"total_rows"` + state attr.ValueState +} + +func (v PaginationValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 5) + + var val tftypes.Value + var err error + + attrTypes["page"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["size"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["sort"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["total_pages"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["total_rows"] = basetypes.Int64Type{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 5) + + val, err = v.Page.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["page"] = val + + val, err = v.Size.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["size"] = val + + val, err = v.Sort.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["sort"] = val + + val, err = v.TotalPages.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["total_pages"] = val + + val, err = v.TotalRows.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["total_rows"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v PaginationValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v PaginationValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v PaginationValue) String() string { + return "PaginationValue" +} + +func (v PaginationValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "page": basetypes.Int64Type{}, + "size": basetypes.Int64Type{}, + "sort": basetypes.StringType{}, + "total_pages": basetypes.Int64Type{}, + "total_rows": basetypes.Int64Type{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "page": v.Page, + "size": v.Size, + "sort": v.Sort, + "total_pages": v.TotalPages, + "total_rows": v.TotalRows, + }) + + return objVal, diags +} + +func (v PaginationValue) Equal(o attr.Value) bool { + other, ok := o.(PaginationValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Page.Equal(other.Page) { + return false + } + + if !v.Size.Equal(other.Size) { + return false + } + + if !v.Sort.Equal(other.Sort) { + return false + } + + if !v.TotalPages.Equal(other.TotalPages) { + return false + } + + if !v.TotalRows.Equal(other.TotalRows) { + return false + } + + return true +} + +func (v PaginationValue) Type(ctx context.Context) attr.Type { + return PaginationType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v PaginationValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "page": basetypes.Int64Type{}, + "size": basetypes.Int64Type{}, + "sort": basetypes.StringType{}, + "total_pages": basetypes.Int64Type{}, + "total_rows": basetypes.Int64Type{}, + } +} diff --git a/stackit/internal/services/sqlserverflexbeta/versions/datasources_gen/version_data_source_gen.go b/stackit/internal/services/sqlserverflexbeta/versions/datasources_gen/version_data_source_gen.go new file mode 100644 index 00000000..239b44d3 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/versions/datasources_gen/version_data_source_gen.go @@ -0,0 +1,569 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package sqlserverflexbeta + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func VersionDataSourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "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", + ), + }, + }, + "versions": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "beta": schema.BoolAttribute{ + Computed: true, + Description: "Flag if the version is a beta version. If set the version may contain bugs and is not fully tested.", + MarkdownDescription: "Flag if the version is a beta version. If set the version may contain bugs and is not fully tested.", + }, + "deprecated": schema.StringAttribute{ + Computed: true, + Description: "Timestamp in RFC3339 format which says when the version will no longer be supported by STACKIT.", + MarkdownDescription: "Timestamp in RFC3339 format which says when the version will no longer be supported by STACKIT.", + }, + "recommend": schema.BoolAttribute{ + Computed: true, + Description: "Flag if the version is recommend by the STACKIT Team.", + MarkdownDescription: "Flag if the version is recommend by the STACKIT Team.", + }, + "version": schema.StringAttribute{ + Computed: true, + Description: "The sqlserver version used for the instance.", + MarkdownDescription: "The sqlserver version used for the instance.", + }, + }, + CustomType: VersionsType{ + ObjectType: types.ObjectType{ + AttrTypes: VersionsValue{}.AttributeTypes(ctx), + }, + }, + }, + Computed: true, + Description: "A list containing available sqlserver versions.", + MarkdownDescription: "A list containing available sqlserver versions.", + }, + }, + } +} + +type VersionModel struct { + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Versions types.List `tfsdk:"versions"` +} + +var _ basetypes.ObjectTypable = VersionsType{} + +type VersionsType struct { + basetypes.ObjectType +} + +func (t VersionsType) Equal(o attr.Type) bool { + other, ok := o.(VersionsType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t VersionsType) String() string { + return "VersionsType" +} + +func (t VersionsType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + betaAttribute, ok := attributes["beta"] + + if !ok { + diags.AddError( + "Attribute Missing", + `beta is missing from object`) + + return nil, diags + } + + betaVal, ok := betaAttribute.(basetypes.BoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`beta expected to be basetypes.BoolValue, was: %T`, betaAttribute)) + } + + deprecatedAttribute, ok := attributes["deprecated"] + + if !ok { + diags.AddError( + "Attribute Missing", + `deprecated is missing from object`) + + return nil, diags + } + + deprecatedVal, ok := deprecatedAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`deprecated expected to be basetypes.StringValue, was: %T`, deprecatedAttribute)) + } + + recommendAttribute, ok := attributes["recommend"] + + if !ok { + diags.AddError( + "Attribute Missing", + `recommend is missing from object`) + + return nil, diags + } + + recommendVal, ok := recommendAttribute.(basetypes.BoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`recommend expected to be basetypes.BoolValue, was: %T`, recommendAttribute)) + } + + versionAttribute, ok := attributes["version"] + + if !ok { + diags.AddError( + "Attribute Missing", + `version is missing from object`) + + return nil, diags + } + + versionVal, ok := versionAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`version expected to be basetypes.StringValue, was: %T`, versionAttribute)) + } + + if diags.HasError() { + return nil, diags + } + + return VersionsValue{ + Beta: betaVal, + Deprecated: deprecatedVal, + Recommend: recommendVal, + Version: versionVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewVersionsValueNull() VersionsValue { + return VersionsValue{ + state: attr.ValueStateNull, + } +} + +func NewVersionsValueUnknown() VersionsValue { + return VersionsValue{ + state: attr.ValueStateUnknown, + } +} + +func NewVersionsValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (VersionsValue, diag.Diagnostics) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing VersionsValue Attribute Value", + "While creating a VersionsValue value, a missing attribute value was detected. "+ + "A VersionsValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("VersionsValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid VersionsValue Attribute Type", + "While creating a VersionsValue value, an invalid attribute value was detected. "+ + "A VersionsValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("VersionsValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("VersionsValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra VersionsValue Attribute Value", + "While creating a VersionsValue value, an extra attribute value was detected. "+ + "A VersionsValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra VersionsValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewVersionsValueUnknown(), diags + } + + betaAttribute, ok := attributes["beta"] + + if !ok { + diags.AddError( + "Attribute Missing", + `beta is missing from object`) + + return NewVersionsValueUnknown(), diags + } + + betaVal, ok := betaAttribute.(basetypes.BoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`beta expected to be basetypes.BoolValue, was: %T`, betaAttribute)) + } + + deprecatedAttribute, ok := attributes["deprecated"] + + if !ok { + diags.AddError( + "Attribute Missing", + `deprecated is missing from object`) + + return NewVersionsValueUnknown(), diags + } + + deprecatedVal, ok := deprecatedAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`deprecated expected to be basetypes.StringValue, was: %T`, deprecatedAttribute)) + } + + recommendAttribute, ok := attributes["recommend"] + + if !ok { + diags.AddError( + "Attribute Missing", + `recommend is missing from object`) + + return NewVersionsValueUnknown(), diags + } + + recommendVal, ok := recommendAttribute.(basetypes.BoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`recommend expected to be basetypes.BoolValue, was: %T`, recommendAttribute)) + } + + versionAttribute, ok := attributes["version"] + + if !ok { + diags.AddError( + "Attribute Missing", + `version is missing from object`) + + return NewVersionsValueUnknown(), diags + } + + versionVal, ok := versionAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`version expected to be basetypes.StringValue, was: %T`, versionAttribute)) + } + + if diags.HasError() { + return NewVersionsValueUnknown(), diags + } + + return VersionsValue{ + Beta: betaVal, + Deprecated: deprecatedVal, + Recommend: recommendVal, + Version: versionVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewVersionsValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) VersionsValue { + object, diags := NewVersionsValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append(diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail())) + } + + panic("NewVersionsValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t VersionsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewVersionsValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewVersionsValueUnknown(), nil + } + + if in.IsNull() { + return NewVersionsValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewVersionsValueMust(VersionsValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t VersionsType) ValueType(ctx context.Context) attr.Value { + return VersionsValue{} +} + +var _ basetypes.ObjectValuable = VersionsValue{} + +type VersionsValue struct { + Beta basetypes.BoolValue `tfsdk:"beta"` + Deprecated basetypes.StringValue `tfsdk:"deprecated"` + Recommend basetypes.BoolValue `tfsdk:"recommend"` + Version basetypes.StringValue `tfsdk:"version"` + state attr.ValueState +} + +func (v VersionsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 4) + + var val tftypes.Value + var err error + + attrTypes["beta"] = basetypes.BoolType{}.TerraformType(ctx) + attrTypes["deprecated"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["recommend"] = basetypes.BoolType{}.TerraformType(ctx) + attrTypes["version"] = basetypes.StringType{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 4) + + val, err = v.Beta.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["beta"] = val + + val, err = v.Deprecated.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["deprecated"] = val + + val, err = v.Recommend.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["recommend"] = val + + val, err = v.Version.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["version"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v VersionsValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v VersionsValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v VersionsValue) String() string { + return "VersionsValue" +} + +func (v VersionsValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "beta": basetypes.BoolType{}, + "deprecated": basetypes.StringType{}, + "recommend": basetypes.BoolType{}, + "version": basetypes.StringType{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "beta": v.Beta, + "deprecated": v.Deprecated, + "recommend": v.Recommend, + "version": v.Version, + }) + + return objVal, diags +} + +func (v VersionsValue) Equal(o attr.Value) bool { + other, ok := o.(VersionsValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Beta.Equal(other.Beta) { + return false + } + + if !v.Deprecated.Equal(other.Deprecated) { + return false + } + + if !v.Recommend.Equal(other.Recommend) { + return false + } + + if !v.Version.Equal(other.Version) { + return false + } + + return true +} + +func (v VersionsValue) Type(ctx context.Context) attr.Type { + return VersionsType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v VersionsValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "beta": basetypes.BoolType{}, + "deprecated": basetypes.StringType{}, + "recommend": basetypes.BoolType{}, + "version": basetypes.StringType{}, + } +} -- 2.49.1 From 615f88579bea8a49b5b23c5414a13221d4c4e664 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:39:49 +0100 Subject: [PATCH 03/31] feat: add database resource schema for PostgreSQL flex alpha --- .../datasources_gen/database_data_source_gen.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go index d5683a6c..a890b593 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go @@ -4,7 +4,9 @@ package postgresflexalpha import ( "context" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -31,14 +33,21 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { }, "name": schema.StringAttribute{ Computed: true, + Optional: true, Description: "The name of the database.", MarkdownDescription: "The name of the database.", }, "owner": schema.StringAttribute{ + Optional: true, Computed: true, Description: "The owner of the database.", MarkdownDescription: "The owner of the database.", }, + "instance_id": schema.StringAttribute{ + Required: true, + Description: "The ID of the instance.", + MarkdownDescription: "The ID of the instance.", + }, "project_id": schema.StringAttribute{ Required: true, Description: "The STACKIT project ID.", @@ -64,6 +73,7 @@ type DatabaseModel struct { InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Owner types.String `tfsdk:"owner"` + InstanceId types.String `tfsdk:"instance_id"` ProjectId types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` } -- 2.49.1 From 3f9abd525cb2c527a3cf853ea2319617a845150a Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:40:09 +0100 Subject: [PATCH 04/31] feat: implement field mapping for ListDatabase API response --- .../postgresflexalpha/database/mapper.go | 47 ++++++ .../postgresflexalpha/database/mapper_test.go | 157 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 stackit/internal/services/postgresflexalpha/database/mapper.go create mode 100644 stackit/internal/services/postgresflexalpha/database/mapper_test.go diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go new file mode 100644 index 00000000..4a9043d1 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -0,0 +1,47 @@ +package postgresflexalpha + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/types" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" +) + +// mapFields maps fields from a ListDatabase API response to a DatabaseModel for the data source. +func mapFields(resp *postgresflexalpha.ListDatabase, model *postgresflexalpha2.DatabaseModel, 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.Id.ValueInt64() != 0 { + databaseId = model.Id.ValueInt64() + } else if resp.Id != nil { + databaseId = *resp.Id + } else { + return fmt.Errorf("database id not present") + } + + model.Id = types.Int64Value(databaseId) + model.Name = types.StringPointerValue(resp.Name) + model.Region = types.StringValue(region) + model.Owner = types.StringPointerValue(cleanString(resp.Owner)) + return nil +} + +// cleanString removes leading and trailing quotes from a string pointer. +func cleanString(s *string) *string { + if s == nil { + return nil + } + res := strings.Trim(*s, "\"") + return &res +} diff --git a/stackit/internal/services/postgresflexalpha/database/mapper_test.go b/stackit/internal/services/postgresflexalpha/database/mapper_test.go new file mode 100644 index 00000000..f36aab99 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/mapper_test.go @@ -0,0 +1,157 @@ +package postgresflexalpha + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" +) + +func TestMapFields(t *testing.T) { + type input struct { + resp *postgresflexalpha.ListDatabase + model *postgresflexalpha2.DatabaseModel + region string + } + type expected struct { + model *postgresflexalpha2.DatabaseModel + err bool + } + + testcases := []struct { + name string + given input + want expected + }{ + { + name: "should map fields correctly when given a valid response", + given: input{ + resp: &postgresflexalpha.ListDatabase{ + Id: ptr(int64(123)), + Name: ptr("my-db"), + Owner: ptr("\"my-owner\""), + }, + model: &postgresflexalpha2.DatabaseModel{}, + region: "eu01", + }, + want: expected{ + model: &postgresflexalpha2.DatabaseModel{ + Id: types.Int64Value(123), + Name: types.StringValue("my-db"), + Region: types.StringValue("eu01"), + Owner: types.StringValue("my-owner"), + }, + err: false, + }, + }, + { + name: "should use existing model ID if present", + given: input{ + resp: &postgresflexalpha.ListDatabase{ + Id: ptr(int64(456)), + Name: ptr("my-db"), + }, + model: &postgresflexalpha2.DatabaseModel{ + Id: types.Int64Value(789), + }, + region: "eu01", + }, + want: expected{ + model: &postgresflexalpha2.DatabaseModel{ + Id: types.Int64Value(789), + Name: types.StringValue("my-db"), + Region: types.StringValue("eu01"), + Owner: types.StringNull(), + }, + err: false, + }, + }, + { + name: "should return an error when response is nil", + given: input{ + resp: nil, + model: &postgresflexalpha2.DatabaseModel{}, + }, + want: expected{ + err: true, + }, + }, + { + name: "should return an error when response ID is nil", + given: input{ + resp: &postgresflexalpha.ListDatabase{ + Id: nil, + }, + model: &postgresflexalpha2.DatabaseModel{}, + }, + want: expected{ + err: true, + }, + }, + { + name: "should return an error when model is nil", + given: input{ + resp: &postgresflexalpha.ListDatabase{ + Id: ptr(int64(123)), + }, + model: nil, + }, + want: expected{ + err: true, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + err := mapFields(tc.given.resp, tc.given.model, tc.given.region) + if (err != nil) != tc.want.err { + t.Fatalf("expected error: %v, got: %v", tc.want.err, err) + } + if err == nil { + if diff := cmp.Diff(tc.want.model, tc.given.model); diff != "" { + t.Errorf("model mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestCleanString(t *testing.T) { + testcases := []struct { + name string + given *string + want *string + }{ + { + name: "should remove quotes from a string", + given: ptr("\"quoted\""), + want: ptr("quoted"), + }, + { + name: "should return nil for a nil input", + given: nil, + want: nil, + }, + { + name: "should not change a string without quotes", + given: ptr("unquoted"), + want: ptr("unquoted"), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + actual := cleanString(tc.given) + if (actual == nil && tc.want != nil) || (actual != nil && tc.want == nil) || (actual != nil && *actual != *tc.want) { + t.Errorf("expected %v, got %v", tc.want, actual) + } + }) + } +} + +func ptr[T any](v T) *T { + return &v +} -- 2.49.1 From 2454353d24de6bc5910bb9101ae073748d562bf9 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:40:26 +0100 Subject: [PATCH 05/31] fix: update mockRequest methods to use int32 for pagination parameters --- .../services/postgresflexalpha/database/functions_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/functions_test.go b/stackit/internal/services/postgresflexalpha/database/functions_test.go index 7ec941db..a2cdba24 100644 --- a/stackit/internal/services/postgresflexalpha/database/functions_test.go +++ b/stackit/internal/services/postgresflexalpha/database/functions_test.go @@ -12,8 +12,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 } -- 2.49.1 From fae9e05d8c273140792debc1559dae2cd75eca89 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:40:32 +0100 Subject: [PATCH 06/31] feat: add plan modifiers and validators for database fields --- .../database/planModifiers.yaml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 stackit/internal/services/postgresflexalpha/database/planModifiers.yaml diff --git a/stackit/internal/services/postgresflexalpha/database/planModifiers.yaml b/stackit/internal/services/postgresflexalpha/database/planModifiers.yaml new file mode 100644 index 00000000..f3f70aeb --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/planModifiers.yaml @@ -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' -- 2.49.1 From a1365ba446e9f473d530588557bd1effbb7beabc Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:56:41 +0100 Subject: [PATCH 07/31] feat: refactor database data source to use new schema and improve error handling --- .../postgresflexalpha/database/datasource.go | 165 +++++-------- .../database/datasource.go.bak | 220 ++++++++++++++++++ 2 files changed, 279 insertions(+), 106 deletions(-) create mode 100644 stackit/internal/services/postgresflexalpha/database/datasource.go.bak diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 36fc5333..d6890954 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -5,18 +5,16 @@ import ( "fmt" "net/http" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" + 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" "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" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" ) @@ -66,132 +64,38 @@ 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.", - } - - 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"], - }, - }, - } +func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = postgresflexalpha2.DatabaseResourceSchema(ctx) } -// 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 postgresflexalpha2.DatabaseModel 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 +122,52 @@ func (r *databaseDataSource) Read( } tflog.Info(ctx, "Postgres Flex database read") } + +// getDatabase retrieves a single database by either ID or name. +// It adds a diagnostic if the configuration is invalid. +func (r *databaseDataSource) getDatabaseByNameOrID( + ctx context.Context, + model *postgresflexalpha2.DatabaseModel, + projectId, region, instanceId string, + diags *diag.Diagnostics, +) (*postgresflexalpha.ListDatabase, error) { + isIdSet := !model.Id.IsNull() && !model.Id.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.Id.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 logs and adds a structured error to diagnostics for API errors. +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.StatusNotFound: fmt.Sprintf("Database, instance %q, or project %q not found.", instanceId, projectId), + http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId), + }, + ) +} diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak new file mode 100644 index 00000000..36fc5333 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak @@ -0,0 +1,220 @@ +package postgresflexalpha + +import ( + "context" + "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" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &databaseDataSource{} +) + +// NewDatabaseDataSource is a helper function to simplify the provider implementation. +func NewDatabaseDataSource() datasource.DataSource { + return &databaseDataSource{} +} + +// databaseDataSource is the data source implementation. +type databaseDataSource struct { + client *postgresflexalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (r *databaseDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" +} + +// Configure adds the provider configured client to the data source. +func (r *databaseDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex database client configured") +} + +// 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.", + } + + 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"], + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *databaseDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform + var model Model + 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) + } + + 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), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema and populate Computed attribute values + err = mapFields(databaseResp, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error reading database", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex database read") +} -- 2.49.1 From 6c4ef0f8ac9343a9110c70f9618fbf149e5af3b6 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 08:59:25 +0100 Subject: [PATCH 08/31] test: update error messages in getDatabase tests for clarity --- .../services/postgresflexalpha/database/functions_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/functions_test.go b/stackit/internal/services/postgresflexalpha/database/functions_test.go index a2cdba24..5233478e 100644 --- a/stackit/internal/services/postgresflexalpha/database/functions_test.go +++ b/stackit/internal/services/postgresflexalpha/database/functions_test.go @@ -176,18 +176,18 @@ 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) } } }, -- 2.49.1 From 8fec464c3182640efbd1e47cf4d78b0a8ff96707 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 09:02:34 +0100 Subject: [PATCH 09/31] feat: enhance error messages for invalid request parameters and not found cases --- .../services/postgresflexalpha/database/datasource.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index d6890954..45bd305c 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -166,7 +166,16 @@ func handleReadError(ctx context.Context, diags *diag.Diagnostics, err error, pr projectId, ), map[int]string{ - http.StatusNotFound: fmt.Sprintf("Database, instance %q, or project %q not found.", instanceId, projectId), + 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), }, ) -- 2.49.1 From 4830d6196de3f924b2f28c8e564fed12b3212963 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 20:18:46 +0100 Subject: [PATCH 10/31] feat: extend database data source schema with project_id, instance_id, region, and tf_id attributes --- .../postgresflexalpha/database/datasource.go | 47 ++++++++++++++++--- .../database_data_source_gen.go | 22 --------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 45bd305c..28d8e51d 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -5,7 +5,9 @@ import ( "fmt" "net/http" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" 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" @@ -18,6 +20,15 @@ import ( "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" ) +// DataSourceModel maps the data source schema data. +type DataSourceModel struct { + postgresflexalpha2.DatabaseModel + ProjectId types.String `tfsdk:"project_id"` + InstanceId types.String `tfsdk:"instance_id"` + Region types.String `tfsdk:"region"` + TerraformID types.String `tfsdk:"tf_id"` +} + // Ensure the implementation satisfies the expected interfaces. var ( _ datasource.DataSource = &databaseDataSource{} @@ -65,7 +76,30 @@ func (r *databaseDataSource) Configure( // Schema defines the schema for the data source. func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = postgresflexalpha2.DatabaseResourceSchema(ctx) + + s := postgresflexalpha2.DatabaseResourceSchema(ctx) + s.Attributes["project_id"] = schema.StringAttribute{ + Description: "STACKIT project ID to which the instance is associated.", + MarkdownDescription: "STACKIT project ID to which the instance is associated.", + Required: true, + } + s.Attributes["instance_id"] = schema.StringAttribute{ + Description: "ID of the PostgresFlex instance.", + MarkdownDescription: "ID of the PostgresFlex instance.", + Required: true, + } + s.Attributes["region"] = schema.StringAttribute{ + Description: "Region of the PostgresFlex instance.", + MarkdownDescription: "Region of the PostgresFlex instance.", + Optional: true, + } + s.Attributes["tf_id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`id`\\\".\",", + Optional: true, + Computed: true, + } + + resp.Schema = s } // Read fetches the data for the data source. @@ -74,7 +108,7 @@ func (r *databaseDataSource) Read( req datasource.ReadRequest, resp *datasource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model postgresflexalpha2.DatabaseModel + var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -123,11 +157,10 @@ func (r *databaseDataSource) Read( tflog.Info(ctx, "Postgres Flex database read") } -// getDatabase retrieves a single database by either ID or name. -// It adds a diagnostic if the configuration is invalid. +// getDatabaseByNameOrID retrieves a single database by ensuring either a unique ID or name is provided. func (r *databaseDataSource) getDatabaseByNameOrID( ctx context.Context, - model *postgresflexalpha2.DatabaseModel, + model *DataSourceModel, projectId, region, instanceId string, diags *diag.Diagnostics, ) (*postgresflexalpha.ListDatabase, error) { @@ -144,7 +177,7 @@ func (r *databaseDataSource) getDatabaseByNameOrID( if isIdSet { databaseId := model.Id.ValueInt64() - ctx = tflog.SetField(ctx, "database_id", databaseId) + ctx = tflog.SetField(ctx, "id", databaseId) return getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) } @@ -153,7 +186,7 @@ func (r *databaseDataSource) getDatabaseByNameOrID( return getDatabaseByName(ctx, r.client, projectId, region, instanceId, databaseName) } -// handleReadError logs and adds a structured error to diagnostics for API errors. +// 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, diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go index a890b593..cf4b987c 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go @@ -5,9 +5,7 @@ package postgresflexalpha import ( "context" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -43,26 +41,6 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { Description: "The owner of the database.", MarkdownDescription: "The owner of the database.", }, - "instance_id": schema.StringAttribute{ - Required: true, - Description: "The ID of the instance.", - MarkdownDescription: "The ID of the instance.", - }, - "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", - ), - }, - }, }, } } -- 2.49.1 From 936ad54f9522465b063064d2ef9e33c560e2cca3 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 20:18:53 +0100 Subject: [PATCH 11/31] feat: enhance mapping functions for ListDatabase API response and improve error handling --- .../postgresflexalpha/database/mapper.go | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 4a9043d1..6e944361 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -6,15 +6,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" - postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" ) -// mapFields maps fields from a ListDatabase API response to a DatabaseModel for the data source. -func mapFields(resp *postgresflexalpha.ListDatabase, model *postgresflexalpha2.DatabaseModel, region string) error { - if resp == nil { +// 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 resp.Id == nil || *resp.Id == 0 { + if source.Id == nil || *source.Id == 0 { return fmt.Errorf("id not present") } if model == nil { @@ -24,20 +27,59 @@ func mapFields(resp *postgresflexalpha.ListDatabase, model *postgresflexalpha2.D var databaseId int64 if model.Id.ValueInt64() != 0 { databaseId = model.Id.ValueInt64() - } else if resp.Id != nil { - databaseId = *resp.Id + } else if source.Id != nil { + databaseId = *source.Id } else { return fmt.Errorf("database id not present") } model.Id = types.Int64Value(databaseId) - model.Name = types.StringPointerValue(resp.Name) - model.Region = types.StringValue(region) - model.Owner = types.StringPointerValue(cleanString(resp.Owner)) + model.Name = types.StringPointerValue(source.Name) + model.Owner = types.StringPointerValue(cleanString(source.Owner)) + return nil } -// cleanString removes leading and trailing quotes from a string pointer. +// 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.Name = types.StringPointerValue(source.Name) + 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 +} + +// cleanString removes leading and trailing quotes which are sometimes returned by the API. func cleanString(s *string) *string { if s == nil { return nil -- 2.49.1 From c96328060da85a06cc1a4b28307be61cc993d93e Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 20:48:40 +0100 Subject: [PATCH 12/31] feat: update mapping functions to include region and improve error handling --- .../postgresflexalpha/database/mapper.go | 3 +- .../postgresflexalpha/database/mapper_test.go | 295 ++++++++++++------ 2 files changed, 206 insertions(+), 92 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 6e944361..560dc38d 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -21,7 +21,7 @@ func mapFields( return fmt.Errorf("id not present") } if model == nil { - return fmt.Errorf("model input is nil") + return fmt.Errorf("model given is nil") } var databaseId int64 @@ -36,6 +36,7 @@ func mapFields( model.Id = types.Int64Value(databaseId) model.Name = types.StringPointerValue(source.Name) model.Owner = types.StringPointerValue(cleanString(source.Owner)) + model.Region = types.StringValue(region) return nil } diff --git a/stackit/internal/services/postgresflexalpha/database/mapper_test.go b/stackit/internal/services/postgresflexalpha/database/mapper_test.go index f36aab99..3fc661a1 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper_test.go @@ -5,153 +5,266 @@ import ( "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" - postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" + datasource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/datasources_gen" + resource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/resources_gen" ) func TestMapFields(t *testing.T) { - type input struct { - resp *postgresflexalpha.ListDatabase - model *postgresflexalpha2.DatabaseModel + type given struct { + source *postgresflexalpha.ListDatabase + model *DataSourceModel region string } type expected struct { - model *postgresflexalpha2.DatabaseModel + model *DataSourceModel err bool } testcases := []struct { - name string - given input - want expected + name string + given given + expected expected }{ { - name: "should map fields correctly when given a valid response", - given: input{ - resp: &postgresflexalpha.ListDatabase{ - Id: ptr(int64(123)), - Name: ptr("my-db"), - Owner: ptr("\"my-owner\""), + 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: &postgresflexalpha2.DatabaseModel{}, + model: &DataSourceModel{}, region: "eu01", }, - want: expected{ - model: &postgresflexalpha2.DatabaseModel{ - Id: types.Int64Value(123), - Name: types.StringValue("my-db"), + 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"), - Owner: types.StringValue("my-owner"), }, - err: false, }, }, { - name: "should use existing model ID if present", - given: input{ - resp: &postgresflexalpha.ListDatabase{ - Id: ptr(int64(456)), - Name: ptr("my-db"), + name: "should preserve existing model ID", + given: given{ + source: &postgresflexalpha.ListDatabase{ + Id: utils.Ptr(int64(1)), + Name: utils.Ptr("my-db"), }, - model: &postgresflexalpha2.DatabaseModel{ - Id: types.Int64Value(789), + model: &DataSourceModel{ + DatabaseModel: datasource.DatabaseModel{ + Id: types.Int64Value(1), + }, }, region: "eu01", }, - want: expected{ - model: &postgresflexalpha2.DatabaseModel{ - Id: types.Int64Value(789), - Name: types.StringValue("my-db"), + expected: expected{ + model: &DataSourceModel{ + DatabaseModel: datasource.DatabaseModel{ + Id: types.Int64Value(1), + Name: types.StringValue("my-db"), + Owner: types.StringNull(), + }, Region: types.StringValue("eu01"), - Owner: types.StringNull(), }, - err: false, }, }, { - name: "should return an error when response is nil", - given: input{ - resp: nil, - model: &postgresflexalpha2.DatabaseModel{}, - }, - want: expected{ - err: true, + name: "should fail on nil source", + given: given{ + source: nil, + model: &DataSourceModel{}, }, + expected: expected{err: true}, }, { - name: "should return an error when response ID is nil", - given: input{ - resp: &postgresflexalpha.ListDatabase{ - Id: nil, - }, - model: &postgresflexalpha2.DatabaseModel{}, - }, - want: 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 return an error when model is nil", - given: input{ - resp: &postgresflexalpha.ListDatabase{ - Id: ptr(int64(123)), - }, - model: nil, - }, - want: 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.resp, tc.given.model, tc.given.region) - if (err != nil) != tc.want.err { - t.Fatalf("expected error: %v, got: %v", tc.want.err, err) - } - if err == nil { - if diff := cmp.Diff(tc.want.model, tc.given.model); diff != "" { - t.Errorf("model mismatch (-want +got):\n%s", diff) + 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{ + DatabaseModel: resource.DatabaseModel{ + Id: types.Int64Value(1), + Name: types.StringValue("my-db"), + Owner: types.StringValue("my-owner"), + }, + }, + }, + }, + { + name: "should fail on nil source", + given: given{ + source: nil, + model: &ResourceModel{}, + }, + expected: expected{err: true}, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + err := mapResourceFields(tc.given.source, tc.given.model) + 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{ + DatabaseModel: resource.DatabaseModel{ + 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) + } + } + }, + ) } } func TestCleanString(t *testing.T) { testcases := []struct { - name string - given *string - want *string + name string + given *string + expected *string }{ { - name: "should remove quotes from a string", - given: ptr("\"quoted\""), - want: ptr("quoted"), + name: "should remove quotes", + given: utils.Ptr("\"quoted\""), + expected: utils.Ptr("quoted"), }, { - name: "should return nil for a nil input", - given: nil, - want: nil, + name: "should handle nil", + given: nil, + expected: nil, }, { - name: "should not change a string without quotes", - given: ptr("unquoted"), - want: ptr("unquoted"), + 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 (actual == nil && tc.want != nil) || (actual != nil && tc.want == nil) || (actual != nil && *actual != *tc.want) { - t.Errorf("expected %v, got %v", tc.want, actual) - } - }) + 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) + } + }, + ) } } - -func ptr[T any](v T) *T { - return &v -} -- 2.49.1 From 502b2f5e0a94eb08ed5a8a80257dbb24dd396fa6 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Wed, 4 Feb 2026 20:48:53 +0100 Subject: [PATCH 13/31] feat: refactor database resource model and enhance identity schema handling --- .../postgresflexalpha/database/resource.go | 336 ++++++----- .../database/resource.go.bak | 539 ++++++++++++++++++ .../database/resource_test.go | 232 -------- 3 files changed, 699 insertions(+), 408 deletions(-) create mode 100644 stackit/internal/services/postgresflexalpha/database/resource.go.bak delete mode 100644 stackit/internal/services/postgresflexalpha/database/resource_test.go diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 67d1e477..b8c79dca 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -2,31 +2,26 @@ 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/identityschema" "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/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. @@ -35,16 +30,21 @@ var ( _ 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"` +// ResourceModel describes the resource data model. +type ResourceModel struct { + postgresflexalpha2.DatabaseModel + TerraformID types.String `tfsdk:"tf_id"` +} + +// 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:"id"` } // NewDatabaseResource is a helper function to simplify the provider implementation. @@ -58,14 +58,13 @@ type databaseResource struct { 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,13 +74,13 @@ func (r *databaseResource) ModifyPlan( return } - var planModel Model + var planModel ResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return } - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + //TODO utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) if resp.Diagnostics.HasError() { return } @@ -117,85 +116,52 @@ 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) + s.Attributes["tf_id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`id`\\\".\",", + Optional: true, + Computed: true, } - 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(), - }, + fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return + } + + err = postgresflexUtils.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(), - }, + "id": identityschema.StringAttribute{ + // database id + RequiredForImport: true, }, }, } @@ -207,18 +173,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 +246,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, @@ -297,23 +271,30 @@ 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 := identityData.ProjectID.ValueString() + instanceId := identityData.InstanceID.ValueString() + databaseId := model.Id.ValueInt64() + region := r.providerData.GetRegionWithOverride(identityData.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) + ctx = tflog.SetField(ctx, "id", databaseId) //database id databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) if err != nil { @@ -329,7 +310,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 +336,40 @@ 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 := identityData.ProjectID.ValueString() + instanceId := identityData.InstanceID.ValueString() + region := r.providerData.GetRegionWithOverride(identityData.Region) + + databaseId64 := model.Id.ValueInt64() 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, "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 +409,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 +434,36 @@ 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 := identityData.ProjectID.ValueString() + instanceId := identityData.InstanceID.ValueString() + region := r.providerData.GetRegionWithOverride(identityData.Region) + databaseId64 := model.Id.ValueInt64() 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, "id", databaseId) //database id // Delete existing record set err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId) @@ -481,7 +477,7 @@ 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],[id] func (r *databaseResource) ImportState( ctx context.Context, req resource.ImportStateRequest, @@ -493,7 +489,7 @@ func (r *databaseResource) ImportState( ctx, &resp.Diagnostics, "Error importing database", fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", + "Expected import identifier with format [project_id],[region],[instance_id],[id], got %q", req.ID, ), ) @@ -503,73 +499,61 @@ func (r *databaseResource) ImportState( 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])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[3])...) //database id 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") -} -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 identityData DatabaseResourceIdentityModel + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + if resp.Diagnostics.HasError() { + return } - var databaseId int64 - if model.DatabaseId.ValueInt64() != 0 { - 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), + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("tf_id"), + utils.BuildInternalTerraformId( + identityData.ProjectID.ValueString(), + identityData.Region.ValueString(), + identityData.InstanceID.ValueString(), + identityData.DatabaseID.String(), + ), + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("project_id"), + identityData.ProjectID.ValueString(), + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, path.Root("region"), identityData.Region.ValueString(), + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("instance_id"), + identityData.InstanceID.ValueString(), + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("id"), // database id + identityData.DatabaseID.ValueInt64(), + )..., ) - 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) -} + tflog.Info(ctx, "Postgres Flex instance state imported") -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") - } - - return &postgresflexalpha.CreateDatabaseRequestPayload{ - Name: model.Name.ValueStringPointer(), - Owner: model.Owner.ValueStringPointer(), - }, nil } var errDatabaseNotFound = errors.New("database not found") diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go.bak b/stackit/internal/services/postgresflexalpha/database/resource.go.bak new file mode 100644 index 00000000..06bf2021 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/resource.go.bak @@ -0,0 +1,539 @@ +package postgresflexalpha + +import ( + "context" + "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/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/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/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &databaseResource{} + _ resource.ResourceWithConfigure = &databaseResource{} + _ resource.ResourceWithImportState = &databaseResource{} + _ resource.ResourceWithModifyPlan = &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"` +} + +// NewDatabaseResource is a helper function to simplify the provider implementation. +func NewDatabaseResource() resource.Resource { + return &databaseResource{} +} + +// 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. +func (r *databaseResource) ModifyPlan( + ctx context.Context, + req resource.ModifyPlanRequest, + resp *resource.ModifyPlanResponse, +) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Metadata returns the resource type name. +func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" +} + +// Configure adds the provider configured client to the resource. +func (r *databaseResource) Configure( + ctx context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex database client configured") +} + +// 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.", + } + + 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(), + }, + }, + "database_id": schema.Int64Attribute{ + Description: descriptions["database_id"], + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + Validators: []validator.Int64{}, + }, + "instance_id": schema.StringAttribute{ + Description: descriptions["instance_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "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(), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *databaseResource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "region", region) + + // Generate API request body from model + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating database", + fmt.Sprintf("Creating API payload: %v", err), + ) + return + } + // Create new database + databaseResp, err := r.client.CreateDatabaseRequest( + ctx, + projectId, + region, + instanceId, + ).CreateDatabaseRequestPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + if databaseResp == nil || databaseResp.Id == nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating database", + "API didn't return database Id. A database might have been created", + ) + return + } + databaseId := *databaseResp.Id + ctx = tflog.SetField(ctx, "database_id", databaseId) + + database, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating database", + fmt.Sprintf("Getting database details after creation: %v", err), + ) + return + } + + // Map response body to schema + err = mapFields(database, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating database", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex database created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *databaseResource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + 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) + + databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFields(databaseResp, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error reading database", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex database read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *databaseResource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + databaseId64 := model.DatabaseId.ValueInt64() + 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) + + // Retrieve values from state + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + modified := false + var payload postgresflexalpha.UpdateDatabasePartiallyRequestPayload + if stateModel.Name != model.Name { + payload.Name = model.Name.ValueStringPointer() + modified = true + } + + if stateModel.Owner != model.Owner { + payload.Owner = model.Owner.ValueStringPointer() + modified = true + } + + if !modified { + tflog.Info(ctx, "no modification detected") + return + } + + // Update existing database + res, err := r.client.UpdateDatabasePartiallyRequest( + ctx, + projectId, + region, + instanceId, + databaseId, + ).UpdateDatabasePartiallyRequestPayload(payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "error updating database", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFieldsUpdatePartially(res, &model, region) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error updating database", + fmt.Sprintf("Processing API payload: %v", err), + ) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Postgres Flex database updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *databaseResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + databaseId64 := model.DatabaseId.ValueInt64() + + 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) + + // Delete existing record set + err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) + } + + ctx = core.LogResponse(ctx) + + tflog.Info(ctx, "Postgres Flex database deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +func (r *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, + ), + ) + 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.", + ) + tflog.Info(ctx, "Postgres Flex database state imported") +} + +func mapFieldsUpdatePartially( + res *postgresflexalpha.UpdateDatabasePartiallyResponse, + model *postgresflexalpha2.DatabaseModel, + region string, +) error { + if res == nil { + return fmt.Errorf("response is nil") + } + return mapFields(res.Database, model, region) +} + +func toCreatePayload(model *Model) (*postgresflexalpha.CreateDatabaseRequestPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + return &postgresflexalpha.CreateDatabaseRequestPayload{ + Name: model.Name.ValueStringPointer(), + Owner: model.Owner.ValueStringPointer(), + }, nil +} + +var errDatabaseNotFound = errors.New("database not found") diff --git a/stackit/internal/services/postgresflexalpha/database/resource_test.go b/stackit/internal/services/postgresflexalpha/database/resource_test.go deleted file mode 100644 index 15bced10..00000000 --- a/stackit/internal/services/postgresflexalpha/database/resource_test.go +++ /dev/null @@ -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) - } - }) - } -} -- 2.49.1 From 91913c34461bd73df39298729138727e3ac7d5f3 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 09:14:03 +0100 Subject: [PATCH 14/31] feat: add database_id attribute to resource model and update related functions --- .../postgresflexalpha/database/datasource.go | 17 +++++--- .../postgresflexalpha/database/functions.go | 10 +++++ .../database/functions_test.go | 36 ++++++++++++++++ .../postgresflexalpha/database/mapper.go | 12 +----- .../postgresflexalpha/database/mapper_test.go | 42 +++---------------- .../postgresflexalpha/database/resource.go | 38 +++++++++++------ 6 files changed, 90 insertions(+), 65 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 28d8e51d..9212bb0a 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -26,6 +26,7 @@ type DataSourceModel struct { ProjectId types.String `tfsdk:"project_id"` InstanceId types.String `tfsdk:"instance_id"` Region types.String `tfsdk:"region"` + DatabaseID types.Int64 `tfsdk:"database_id"` TerraformID types.String `tfsdk:"tf_id"` } @@ -93,11 +94,17 @@ func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequ MarkdownDescription: "Region of the PostgresFlex instance.", Optional: true, } - s.Attributes["tf_id"] = schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`id`\\\".\",", + s.Attributes["database_id"] = schema.Int64Attribute{ + Description: "The ID of the database.", Optional: true, Computed: true, } + s.Attributes["tf_id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," + + "`database_id`\\\".\",", + Optional: true, + Computed: true, + } resp.Schema = s } @@ -164,7 +171,7 @@ func (r *databaseDataSource) getDatabaseByNameOrID( projectId, region, instanceId string, diags *diag.Diagnostics, ) (*postgresflexalpha.ListDatabase, error) { - isIdSet := !model.Id.IsNull() && !model.Id.IsUnknown() + isIdSet := !model.DatabaseID.IsNull() && !model.DatabaseID.IsUnknown() isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { @@ -176,8 +183,8 @@ func (r *databaseDataSource) getDatabaseByNameOrID( } if isIdSet { - databaseId := model.Id.ValueInt64() - ctx = tflog.SetField(ctx, "id", databaseId) + databaseId := model.DatabaseID.ValueInt64() + ctx = tflog.SetField(ctx, "database_id", databaseId) return getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) } diff --git a/stackit/internal/services/postgresflexalpha/database/functions.go b/stackit/internal/services/postgresflexalpha/database/functions.go index b1c30bb9..4496faa1 100644 --- a/stackit/internal/services/postgresflexalpha/database/functions.go +++ b/stackit/internal/services/postgresflexalpha/database/functions.go @@ -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 +} diff --git a/stackit/internal/services/postgresflexalpha/database/functions_test.go b/stackit/internal/services/postgresflexalpha/database/functions_test.go index 5233478e..9f0b47fd 100644 --- a/stackit/internal/services/postgresflexalpha/database/functions_test.go +++ b/stackit/internal/services/postgresflexalpha/database/functions_test.go @@ -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" ) @@ -194,3 +195,38 @@ func TestGetDatabase(t *testing.T) { ) } } + +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) + } + }, + ) + } +} diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 560dc38d..69830e45 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -2,7 +2,6 @@ package postgresflexalpha import ( "fmt" - "strings" "github.com/hashicorp/terraform-plugin-framework/types" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" @@ -34,6 +33,7 @@ func mapFields( } model.Id = types.Int64Value(databaseId) + model.DatabaseID = types.Int64Value(databaseId) model.Name = types.StringPointerValue(source.Name) model.Owner = types.StringPointerValue(cleanString(source.Owner)) model.Region = types.StringValue(region) @@ -63,6 +63,7 @@ func mapResourceFields(source *postgresflexalpha.ListDatabase, model *ResourceMo } model.Id = types.Int64Value(databaseId) + model.DatabaseID = types.Int64Value(databaseId) model.Name = types.StringPointerValue(source.Name) model.Owner = types.StringPointerValue(cleanString(source.Owner)) return nil @@ -79,12 +80,3 @@ func toCreatePayload(model *ResourceModel) (*postgresflexalpha.CreateDatabaseReq Owner: model.Owner.ValueStringPointer(), }, nil } - -// 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 -} diff --git a/stackit/internal/services/postgresflexalpha/database/mapper_test.go b/stackit/internal/services/postgresflexalpha/database/mapper_test.go index 3fc661a1..ead6b1e3 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper_test.go @@ -45,7 +45,8 @@ func TestMapFields(t *testing.T) { Name: types.StringValue("my-db"), Owner: types.StringValue("my-owner"), }, - Region: types.StringValue("eu01"), + Region: types.StringValue("eu01"), + DatabaseID: types.Int64Value(1), }, }, }, @@ -70,7 +71,8 @@ func TestMapFields(t *testing.T) { Name: types.StringValue("my-db"), Owner: types.StringNull(), }, - Region: types.StringValue("eu01"), + DatabaseID: types.Int64Value(1), + Region: types.StringValue("eu01"), }, }, }, @@ -149,6 +151,7 @@ func TestMapResourceFields(t *testing.T) { Name: types.StringValue("my-db"), Owner: types.StringValue("my-owner"), }, + DatabaseID: types.Int64Value(1), }, }, }, @@ -233,38 +236,3 @@ func TestToCreatePayload(t *testing.T) { ) } } - -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) - } - }, - ) - } -} diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index b8c79dca..ca8230a3 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -13,6 +13,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" @@ -37,6 +40,7 @@ var ( type ResourceModel struct { postgresflexalpha2.DatabaseModel TerraformID types.String `tfsdk:"tf_id"` + DatabaseID types.Int64 `tfsdk:"database_id"` } // DatabaseResourceIdentityModel describes the resource's identity attributes. @@ -44,7 +48,7 @@ type DatabaseResourceIdentityModel struct { ProjectID types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` InstanceID types.String `tfsdk:"instance_id"` - DatabaseID types.Int64 `tfsdk:"id"` + DatabaseID types.Int64 `tfsdk:"database_id"` } // NewDatabaseResource is a helper function to simplify the provider implementation. @@ -123,10 +127,18 @@ var modifiersFileByte []byte func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha2.DatabaseResourceSchema(ctx) s.Attributes["tf_id"] = schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`id`\\\".\",", + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", Optional: true, Computed: true, } + s.Attributes["database_id"] = schema.Int64Attribute{ + Description: "ID of the database.", + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + Validators: []validator.Int64{}, + } fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) if err != nil { @@ -159,7 +171,7 @@ func (r *databaseResource) IdentitySchema( "instance_id": identityschema.StringAttribute{ RequiredForImport: true, }, - "id": identityschema.StringAttribute{ + "database_id": identityschema.Int64Attribute{ // database id RequiredForImport: true, }, @@ -289,12 +301,12 @@ func (r *databaseResource) Read( projectId := identityData.ProjectID.ValueString() instanceId := identityData.InstanceID.ValueString() - databaseId := model.Id.ValueInt64() + databaseId := model.DatabaseID.ValueInt64() region := r.providerData.GetRegionWithOverride(identityData.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "id", databaseId) //database id + ctx = tflog.SetField(ctx, "database_id", databaseId) databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) if err != nil { @@ -356,7 +368,7 @@ func (r *databaseResource) Update( instanceId := identityData.InstanceID.ValueString() region := r.providerData.GetRegionWithOverride(identityData.Region) - databaseId64 := model.Id.ValueInt64() + databaseId64 := model.DatabaseID.ValueInt64() // database id if databaseId64 > math.MaxInt32 { core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") return @@ -366,7 +378,7 @@ func (r *databaseResource) Update( ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "id", databaseId) + ctx = tflog.SetField(ctx, "database_id", databaseId) // Retrieve values from state var stateModel ResourceModel @@ -453,7 +465,7 @@ func (r *databaseResource) Delete( projectId := identityData.ProjectID.ValueString() instanceId := identityData.InstanceID.ValueString() region := r.providerData.GetRegionWithOverride(identityData.Region) - databaseId64 := model.Id.ValueInt64() + databaseId64 := model.DatabaseID.ValueInt64() //database id if databaseId64 > math.MaxInt32 { core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") @@ -463,7 +475,7 @@ func (r *databaseResource) Delete( ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "id", databaseId) //database id + ctx = tflog.SetField(ctx, "database_id", databaseId) // Delete existing record set err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId) @@ -477,7 +489,7 @@ func (r *databaseResource) Delete( } // ImportState imports a resource into the Terraform state on success. -// The expected import identifier format is: [project_id],[region],[instance_id],[id] +// The expected import identifier format is: [project_id],[region],[instance_id],[database_id] func (r *databaseResource) ImportState( ctx context.Context, req resource.ImportStateRequest, @@ -489,7 +501,7 @@ func (r *databaseResource) ImportState( ctx, &resp.Diagnostics, "Error importing database", fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[id], got %q", + "Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", req.ID, ), ) @@ -499,7 +511,7 @@ func (r *databaseResource) ImportState( 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("id"), idParts[3])...) //database id + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), idParts[3])...) core.LogAndAddWarning( ctx, &resp.Diagnostics, @@ -547,7 +559,7 @@ func (r *databaseResource) ImportState( resp.Diagnostics.Append( resp.State.SetAttribute( ctx, - path.Root("id"), // database id + path.Root("database_id"), identityData.DatabaseID.ValueInt64(), )..., ) -- 2.49.1 From 546eafcb2fdbfeb4282bc8307906647d160fd373 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:07:23 +0100 Subject: [PATCH 15/31] feat: refactor Terraform ID handling and add user mapping functions --- .../postgresflexalpha/database/datasource.go | 4 +- .../postgresflexalpha/database/resource.go | 85 +-- .../services/postgresflexalpha/user/mapper.go | 150 +++++ .../postgresflexalpha/user/mapper_test.go | 632 ++++++++++++++++++ 4 files changed, 816 insertions(+), 55 deletions(-) create mode 100644 stackit/internal/services/postgresflexalpha/user/mapper.go create mode 100644 stackit/internal/services/postgresflexalpha/user/mapper_test.go diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 9212bb0a..3e526242 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -27,7 +27,7 @@ type DataSourceModel struct { InstanceId types.String `tfsdk:"instance_id"` Region types.String `tfsdk:"region"` DatabaseID types.Int64 `tfsdk:"database_id"` - TerraformID types.String `tfsdk:"tf_id"` + TerraformID types.String `tfsdk:"id"` } // Ensure the implementation satisfies the expected interfaces. @@ -99,7 +99,7 @@ func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequ Optional: true, Computed: true, } - s.Attributes["tf_id"] = schema.StringAttribute{ + s.Attributes["id"] = schema.StringAttribute{ Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," + "`database_id`\\\".\",", Optional: true, diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index ca8230a3..169c3e99 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "net/http" + "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/path" @@ -24,7 +25,6 @@ import ( "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" ) // Ensure the implementation satisfies the expected interfaces. @@ -34,12 +34,14 @@ var ( _ resource.ResourceWithImportState = &databaseResource{} _ resource.ResourceWithModifyPlan = &databaseResource{} _ resource.ResourceWithIdentity = &databaseResource{} + + errDatabaseNotFound = errors.New("database not found") ) // ResourceModel describes the resource data model. type ResourceModel struct { postgresflexalpha2.DatabaseModel - TerraformID types.String `tfsdk:"tf_id"` + TerraformID types.String `tfsdk:"id"` DatabaseID types.Int64 `tfsdk:"database_id"` } @@ -126,7 +128,7 @@ var modifiersFileByte []byte // Schema defines the schema for the resource. func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha2.DatabaseResourceSchema(ctx) - s.Attributes["tf_id"] = schema.StringAttribute{ + s.Attributes["id"] = schema.StringAttribute{ Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", Optional: true, Computed: true, @@ -172,7 +174,6 @@ func (r *databaseResource) IdentitySchema( RequiredForImport: true, }, "database_id": identityschema.Int64Attribute{ - // database id RequiredForImport: true, }, }, @@ -508,64 +509,42 @@ func (r *databaseResource) ImportState( 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"), 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.", - ) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), databaseId)...) + + //TODO: Investigate if this logic is still required. + //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.", + //) var identityData DatabaseResourceIdentityModel - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + identityData.ProjectID = types.StringValue(idParts[0]) + identityData.Region = types.StringValue(idParts[1]) + identityData.InstanceID = types.StringValue(idParts[2]) + identityData.DatabaseID = types.Int64Value(databaseId) + + resp.Diagnostics.Append(req.Identity.Set(ctx, &identityData)...) + if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("tf_id"), - utils.BuildInternalTerraformId( - identityData.ProjectID.ValueString(), - identityData.Region.ValueString(), - identityData.InstanceID.ValueString(), - identityData.DatabaseID.String(), - ), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("project_id"), - identityData.ProjectID.ValueString(), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, path.Root("region"), identityData.Region.ValueString(), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("instance_id"), - identityData.InstanceID.ValueString(), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("database_id"), - identityData.DatabaseID.ValueInt64(), - )..., - ) - tflog.Info(ctx, "Postgres Flex instance state imported") } - -var errDatabaseNotFound = errors.New("database not found") diff --git a/stackit/internal/services/postgresflexalpha/user/mapper.go b/stackit/internal/services/postgresflexalpha/user/mapper.go new file mode 100644 index 00000000..655d7d80 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/user/mapper.go @@ -0,0 +1,150 @@ +package postgresflexalpha + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + postgresflex "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" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" +) + +// mapDataSourceFields maps API response to data source model, preserving existing ID. +func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSourceModel, region string) error { + if userResp == nil { + return fmt.Errorf("response is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + user := userResp + + var userId int64 + if model.UserId.ValueInt64() != 0 { + userId = model.UserId.ValueInt64() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + + model.TerraformID = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), + ) + + model.UserId = types.Int64Value(userId) + model.Name = types.StringValue(user.GetName()) + + if user.Roles == nil { + model.Roles = types.List(types.SetNull(types.StringType)) + } else { + var roles []attr.Value + for _, role := range *user.Roles { + roles = append(roles, types.StringValue(string(role))) + } + rolesSet, diags := types.SetValue(types.StringType, roles) + if diags.HasError() { + return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) + } + model.Roles = types.List(rolesSet) + } + + model.Id = types.Int64Value(userId) + model.Host = types.StringValue(user.GetHost()) + model.Port = types.Int64Value(user.GetPort()) + model.Region = types.StringValue(region) + model.Status = types.StringValue(user.GetStatus()) + model.ConnectionString = types.StringValue(user.GetConnectionString()) + return nil +} + +// toPayloadRoles converts a string slice to the API's role type. +func toPayloadRoles(roles *[]string) *[]postgresflex.UserRole { + var userRoles = make([]postgresflex.UserRole, 0, len(*roles)) + for _, role := range *roles { + userRoles = append(userRoles, postgresflex.UserRole(role)) + } + return &userRoles +} + +// toUpdatePayload creates an API update payload from the resource model. +func toUpdatePayload(model *Model, roles *[]string) ( + *postgresflex.UpdateUserRequestPayload, + error, +) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if roles == nil { + return nil, fmt.Errorf("nil roles") + } + + return &postgresflex.UpdateUserRequestPayload{ + Name: conversion.StringValueToPointer(model.Name), + Roles: toPayloadRoles(roles), + }, nil +} + +// toCreatePayload creates an API create payload from the resource model. +func toCreatePayload(model *Model, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if roles == nil { + return nil, fmt.Errorf("nil roles") + } + + return &postgresflex.CreateUserRequestPayload{ + Roles: toPayloadRoles(roles), + Name: conversion.StringValueToPointer(model.Name), + }, nil +} + +// mapResourceFields maps API response to the resource model, preserving existing ID. +func mapResourceFields(userResp *postgresflex.GetUserResponse, model *Model, region string) error { + if userResp == nil { + return fmt.Errorf("response is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + user := userResp + + var userId int64 + if model.UserId.ValueInt64() != 0 { + userId = model.UserId.ValueInt64() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + model.TerraformID = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), + ) + model.Id = types.Int64Value(userId) + model.UserId = types.Int64Value(userId) + model.Name = types.StringPointerValue(user.Name) + + if user.Roles == nil { + model.Roles = types.List(types.SetNull(types.StringType)) + } else { + var roles []attr.Value + for _, role := range *user.Roles { + roles = append(roles, types.StringValue(string(role))) + } + rolesSet, diags := types.SetValue(types.StringType, roles) + if diags.HasError() { + return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) + } + model.Roles = types.List(rolesSet) + } + model.Host = types.StringPointerValue(user.Host) + model.Port = types.Int64PointerValue(user.Port) + model.Region = types.StringValue(region) + model.Status = types.StringPointerValue(user.Status) + model.ConnectionString = types.StringPointerValue(user.ConnectionString) + return nil +} diff --git a/stackit/internal/services/postgresflexalpha/user/mapper_test.go b/stackit/internal/services/postgresflexalpha/user/mapper_test.go new file mode 100644 index 00000000..863dbbcc --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/user/mapper_test.go @@ -0,0 +1,632 @@ +package postgresflexalpha + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + data "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/datasources_gen" + resource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" +) + +func TestMapDataSourceFields(t *testing.T) { + const testRegion = "region" + tests := []struct { + description string + input *postgresflex.GetUserResponse + region string + expected DataSourceModel + isValid bool + }{ + { + "default_values", + &postgresflex.GetUserResponse{}, + testRegion, + DataSourceModel{ + UserModel: data.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue(""), + Roles: types.List(types.SetNull(types.StringType)), + Host: types.StringValue(""), + Port: types.Int64Value(0), + Status: types.StringValue(""), + Region: types.StringValue(testRegion), + ConnectionString: types.StringValue(""), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "simple_values", + &postgresflex.GetUserResponse{ + Roles: &[]postgresflex.UserRole{ + "role_1", + "role_2", + "", + }, + Name: utils.Ptr("username"), + Host: utils.Ptr("host"), + Port: utils.Ptr(int64(1234)), + }, + testRegion, + DataSourceModel{ + + UserModel: data.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("username"), + Roles: types.List( + types.SetValueMust( + types.StringType, []attr.Value{ + types.StringValue("role_1"), + types.StringValue("role_2"), + types.StringValue(""), + }, + ), + ), + Host: types.StringValue("host"), + Port: types.Int64Value(1234), + Region: types.StringValue(testRegion), + Status: types.StringValue(""), + ConnectionString: types.StringValue(""), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "null_fields_and_int_conversions", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + Roles: &[]postgresflex.UserRole{}, + Name: nil, + Host: nil, + Port: utils.Ptr(int64(2123456789)), + Status: utils.Ptr("status"), + ConnectionString: utils.Ptr("connection_string"), + }, + testRegion, + DataSourceModel{ + UserModel: data.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue(""), + Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})), + Host: types.StringValue(""), + Port: types.Int64Value(2123456789), + Region: types.StringValue(testRegion), + Status: types.StringValue("status"), + ConnectionString: types.StringValue("connection_string"), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "nil_response", + nil, + testRegion, + DataSourceModel{}, + false, + }, + { + "nil_response_2", + &postgresflex.GetUserResponse{}, + testRegion, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + &postgresflex.GetUserResponse{}, + testRegion, + DataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run( + tt.description, func(t *testing.T) { + state := &DataSourceModel{ + UserModel: data.UserModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + UserId: tt.expected.UserId, + }, + } + err := mapDataSourceFields(tt.input, state, tt.region) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(state, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }, + ) + } +} + +func TestMapFieldsCreate(t *testing.T) { + const testRegion = "region" + tests := []struct { + description string + input *postgresflex.GetUserResponse + region string + expected Model + isValid bool + }{ + { + "default_values", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Host: types.StringNull(), + Port: types.Int64Null(), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "simple_values", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + Name: utils.Ptr("username"), + ConnectionString: utils.Ptr("connection_string"), + Status: utils.Ptr("status"), + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("username"), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Host: types.StringNull(), + Port: types.Int64Null(), + Region: types.StringValue(testRegion), + Status: types.StringValue("status"), + ConnectionString: types.StringValue("connection_string"), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "null_fields_and_int_conversions", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + Name: nil, + ConnectionString: nil, + Status: nil, + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Host: types.StringNull(), + Port: types.Int64Null(), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "nil_response", + nil, + testRegion, + Model{}, + false, + }, + { + "nil_response_2", + &postgresflex.GetUserResponse{}, + testRegion, + Model{}, + false, + }, + { + "no_resource_id", + &postgresflex.GetUserResponse{}, + testRegion, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run( + tt.description, func(t *testing.T) { + state := &Model{ + UserModel: resource.UserModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + }, + } + + err := mapResourceFields(tt.input, state, tt.region) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(state, &tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }, + ) + } +} + +func TestMapFields(t *testing.T) { + const testRegion = "region" + tests := []struct { + description string + input *postgresflex.GetUserResponse + region string + expected Model + isValid bool + }{ + { + "default_values", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(int64(1)), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Host: types.StringNull(), + Port: types.Int64Null(), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "simple_values", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + Roles: &[]postgresflex.UserRole{ + "role_1", + "role_2", + "", + }, + Name: utils.Ptr("username"), + Host: utils.Ptr("host"), + Port: utils.Ptr(int64(1234)), + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("username"), + Roles: types.List( + types.SetValueMust( + types.StringType, []attr.Value{ + types.StringValue("role_1"), + types.StringValue("role_2"), + types.StringValue(""), + }, + ), + ), + Host: types.StringValue("host"), + Port: types.Int64Value(1234), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "null_fields_and_int_conversions", + &postgresflex.GetUserResponse{ + Id: utils.Ptr(int64(1)), + Name: nil, + Host: nil, + Port: utils.Ptr(int64(2123456789)), + }, + testRegion, + Model{ + UserModel: resource.UserModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Host: types.StringNull(), + Port: types.Int64Value(2123456789), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), + }, + TerraformID: types.StringValue("pid,region,iid,1"), + }, + true, + }, + { + "nil_response", + nil, + testRegion, + Model{}, + false, + }, + { + "nil_response_2", + &postgresflex.GetUserResponse{}, + testRegion, + Model{}, + false, + }, + { + "no_resource_id", + &postgresflex.GetUserResponse{}, + testRegion, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run( + tt.description, func(t *testing.T) { + state := &Model{ + UserModel: resource.UserModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, + }, + } + err := mapResourceFields(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 + inputRoles *[]string + expected *postgresflex.CreateUserRequestPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &[]string{}, + &postgresflex.CreateUserRequestPayload{ + Name: nil, + Roles: &[]postgresflex.UserRole{}, + }, + true, + }, + { + "simple_values", + &Model{ + UserModel: resource.UserModel{ + Name: types.StringValue("username"), + }, + }, + &[]string{ + "role_1", + "role_2", + }, + &postgresflex.CreateUserRequestPayload{ + Name: utils.Ptr("username"), + Roles: &[]postgresflex.UserRole{ + "role_1", + "role_2", + }, + }, + true, + }, + { + "null_fields_and_int_conversions", + &Model{ + UserModel: resource.UserModel{ + Name: types.StringNull(), + }, + }, + &[]string{ + "", + }, + &postgresflex.CreateUserRequestPayload{ + Roles: &[]postgresflex.UserRole{ + "", + }, + Name: nil, + }, + true, + }, + { + "nil_model", + nil, + &[]string{}, + nil, + false, + }, + { + "nil_roles", + &Model{}, + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run( + tt.description, func(t *testing.T) { + output, err := toCreatePayload(tt.input, tt.inputRoles) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }, + ) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + inputRoles *[]string + expected *postgresflex.UpdateUserRequestPayload + isValid bool + }{ + { + "default_values", + &Model{}, + &[]string{}, + &postgresflex.UpdateUserRequestPayload{ + Roles: &[]postgresflex.UserRole{}, + }, + true, + }, + { + "default_values", + &Model{ + UserModel: resource.UserModel{ + Name: types.StringValue("username"), + }, + }, + &[]string{ + "role_1", + "role_2", + }, + &postgresflex.UpdateUserRequestPayload{ + Name: utils.Ptr("username"), + Roles: &[]postgresflex.UserRole{ + "role_1", + "role_2", + }, + }, + true, + }, + { + "null_fields_and_int_conversions", + &Model{ + UserModel: resource.UserModel{ + Name: types.StringNull(), + }, + }, + &[]string{ + "", + }, + &postgresflex.UpdateUserRequestPayload{ + Roles: &[]postgresflex.UserRole{ + "", + }, + }, + true, + }, + { + "nil_model", + nil, + &[]string{}, + nil, + false, + }, + { + "nil_roles", + &Model{}, + nil, + nil, + false, + }, + } + for _, tt := range tests { + t.Run( + tt.description, func(t *testing.T) { + output, err := toUpdatePayload(tt.input, tt.inputRoles) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }, + ) + } +} -- 2.49.1 From 6802b64faf8e6197fa615753be76779368c84a19 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:07:32 +0100 Subject: [PATCH 16/31] feat: add plan modifiers for user and instance attributes in YAML configuration --- .../postgresflexalpha/user/planModifiers.yaml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 stackit/internal/services/postgresflexalpha/user/planModifiers.yaml diff --git a/stackit/internal/services/postgresflexalpha/user/planModifiers.yaml b/stackit/internal/services/postgresflexalpha/user/planModifiers.yaml new file mode 100644 index 00000000..a7d4cde6 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/user/planModifiers.yaml @@ -0,0 +1,55 @@ +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' + + - name: 'user_id' + modifiers: + - 'UseStateForUnknown' + + - name: 'instance_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'UseStateForUnknown' + + - name: 'project_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'UseStateForUnknown' + - 'RequiresReplace' + + - name: 'name' + modifiers: + - 'UseStateForUnknown' + + - name: 'roles' + modifiers: + - 'UseStateForUnknown' + + - name: 'password' + modifiers: + - 'UseStateForUnknown' + + - name: 'host' + modifiers: + - 'UseStateForUnknown' + + - name: 'port' + modifiers: + - 'UseStateForUnknown' + + - name: 'region' + modifiers: + - 'RequiresReplace' + + - name: 'status' + modifiers: + - 'UseStateForUnknown' + + - name: 'connection_string' + modifiers: + - 'UseStateForUnknown' -- 2.49.1 From bac8ebe236de40e0858adc3b0609d432912a815d Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:08:05 +0100 Subject: [PATCH 17/31] feat: update name assignment to use GetName method in mapper --- .../internal/services/postgresflexalpha/database/mapper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 69830e45..2fa33645 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -34,7 +34,7 @@ func mapFields( model.Id = types.Int64Value(databaseId) model.DatabaseID = types.Int64Value(databaseId) - model.Name = types.StringPointerValue(source.Name) + model.Name = types.StringValue(source.GetName()) model.Owner = types.StringPointerValue(cleanString(source.Owner)) model.Region = types.StringValue(region) @@ -64,7 +64,7 @@ func mapResourceFields(source *postgresflexalpha.ListDatabase, model *ResourceMo model.Id = types.Int64Value(databaseId) model.DatabaseID = types.Int64Value(databaseId) - model.Name = types.StringPointerValue(source.Name) + model.Name = types.StringValue(source.GetName()) model.Owner = types.StringPointerValue(cleanString(source.Owner)) return nil } -- 2.49.1 From 5d92c9b460c1c1078a000eb29efd68cad5cc85f3 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:24:06 +0100 Subject: [PATCH 18/31] feat: refactor user data source and resource schemas to utilize shared models --- .../database/datasource.go.bak | 220 ------- .../database/resource.go.bak | 539 ------------------ .../postgresflexalpha/user/datasource.go | 180 ++---- .../postgresflexalpha/user/datasource_test.go | 146 ----- .../postgresflexalpha/user/resource.go | 216 ++----- .../postgresflexalpha/user/resource_test.go | 448 --------------- 6 files changed, 86 insertions(+), 1663 deletions(-) delete mode 100644 stackit/internal/services/postgresflexalpha/database/datasource.go.bak delete mode 100644 stackit/internal/services/postgresflexalpha/database/resource.go.bak delete mode 100644 stackit/internal/services/postgresflexalpha/user/datasource_test.go delete mode 100644 stackit/internal/services/postgresflexalpha/user/resource_test.go diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak b/stackit/internal/services/postgresflexalpha/database/datasource.go.bak deleted file mode 100644 index 36fc5333..00000000 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go.bak +++ /dev/null @@ -1,220 +0,0 @@ -package postgresflexalpha - -import ( - "context" - "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" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &databaseDataSource{} -) - -// NewDatabaseDataSource is a helper function to simplify the provider implementation. -func NewDatabaseDataSource() datasource.DataSource { - return &databaseDataSource{} -} - -// databaseDataSource is the data source implementation. -type databaseDataSource struct { - client *postgresflexalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *databaseDataSource) Metadata( - _ context.Context, - req datasource.MetadataRequest, - resp *datasource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" -} - -// Configure adds the provider configured client to the data source. -func (r *databaseDataSource) Configure( - ctx context.Context, - req datasource.ConfigureRequest, - resp *datasource.ConfigureResponse, -) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// 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.", - } - - 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"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseDataSource) Read( - ctx context.Context, - req datasource.ReadRequest, - resp *datasource.ReadResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - 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) - } - - 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), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error reading database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go.bak b/stackit/internal/services/postgresflexalpha/database/resource.go.bak deleted file mode 100644 index 06bf2021..00000000 --- a/stackit/internal/services/postgresflexalpha/database/resource.go.bak +++ /dev/null @@ -1,539 +0,0 @@ -package postgresflexalpha - -import ( - "context" - "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/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/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/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &databaseResource{} - _ resource.ResourceWithConfigure = &databaseResource{} - _ resource.ResourceWithImportState = &databaseResource{} - _ resource.ResourceWithModifyPlan = &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"` -} - -// NewDatabaseResource is a helper function to simplify the provider implementation. -func NewDatabaseResource() resource.Resource { - return &databaseResource{} -} - -// 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. -func (r *databaseResource) ModifyPlan( - ctx context.Context, - req resource.ModifyPlanRequest, - resp *resource.ModifyPlanResponse, -) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_database" -} - -// Configure adds the provider configured client to the resource. -func (r *databaseResource) Configure( - ctx context.Context, - req resource.ConfigureRequest, - resp *resource.ConfigureResponse, -) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// 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.", - } - - 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(), - }, - }, - "database_id": schema.Int64Attribute{ - Description: descriptions["database_id"], - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "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(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *databaseResource) Create( - ctx context.Context, - req resource.CreateRequest, - resp *resource.CreateResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Creating API payload: %v", err), - ) - return - } - // Create new database - databaseResp, err := r.client.CreateDatabaseRequest( - ctx, - projectId, - region, - instanceId, - ).CreateDatabaseRequestPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if databaseResp == nil || databaseResp.Id == nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - "API didn't return database Id. A database might have been created", - ) - return - } - databaseId := *databaseResp.Id - ctx = tflog.SetField(ctx, "database_id", databaseId) - - database, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Getting database details after creation: %v", err), - ) - return - } - - // Map response body to schema - err = mapFields(database, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseResource) Read( - ctx context.Context, - req resource.ReadRequest, - resp *resource.ReadResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - 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) - - databaseResp, err := getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error reading database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *databaseResource) Update( - ctx context.Context, - req resource.UpdateRequest, - resp *resource.UpdateResponse, -) { - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId64 := model.DatabaseId.ValueInt64() - 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) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - modified := false - var payload postgresflexalpha.UpdateDatabasePartiallyRequestPayload - if stateModel.Name != model.Name { - payload.Name = model.Name.ValueStringPointer() - modified = true - } - - if stateModel.Owner != model.Owner { - payload.Owner = model.Owner.ValueStringPointer() - modified = true - } - - if !modified { - tflog.Info(ctx, "no modification detected") - return - } - - // Update existing database - res, err := r.client.UpdateDatabasePartiallyRequest( - ctx, - projectId, - region, - instanceId, - databaseId, - ).UpdateDatabasePartiallyRequestPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "error updating database", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFieldsUpdatePartially(res, &model, region) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error updating database", - fmt.Sprintf("Processing API payload: %v", err), - ) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *databaseResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId64 := model.DatabaseId.ValueInt64() - - 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) - - // Delete existing record set - err := r.client.DeleteDatabaseRequestExecute(ctx, projectId, region, instanceId, databaseId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Postgres Flex database deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *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, - ), - ) - 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.", - ) - tflog.Info(ctx, "Postgres Flex database state imported") -} - -func mapFieldsUpdatePartially( - res *postgresflexalpha.UpdateDatabasePartiallyResponse, - model *postgresflexalpha2.DatabaseModel, - region string, -) error { - if res == nil { - return fmt.Errorf("response is nil") - } - return mapFields(res.Database, model, region) -} - -func toCreatePayload(model *Model) (*postgresflexalpha.CreateDatabaseRequestPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &postgresflexalpha.CreateDatabaseRequestPayload{ - Name: model.Name.ValueStringPointer(), - Owner: model.Owner.ValueStringPointer(), - }, nil -} - -var errDatabaseNotFound = errors.New("database not found") diff --git a/stackit/internal/services/postgresflexalpha/user/datasource.go b/stackit/internal/services/postgresflexalpha/user/datasource.go index 7b3a1428..782c5d73 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasource.go +++ b/stackit/internal/services/postgresflexalpha/user/datasource.go @@ -5,22 +5,19 @@ import ( "fmt" "math" "net/http" - "strconv" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" postgresflex "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" + postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/datasources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/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/types" ) // Ensure the implementation satisfies the expected interfaces. @@ -28,18 +25,10 @@ var ( _ datasource.DataSource = &userDataSource{} ) +// DataSourceModel maps the data source schema data. type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.Int64 `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - ConnectionString types.String `tfsdk:"connection_string"` + postgresflexalpha.UserModel + TerraformID types.String `tfsdk:"id"` } // NewUserDataSource is a helper function to simplify the provider implementation. @@ -83,84 +72,16 @@ func (r *userDataSource) Configure( } // Schema defines the schema for the data source. -func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "The name of the user.", - "roles": "The roles assigned to the user.", - "host": "The host address for the user to connect to the instance.", - "port": "The port number for the user to connect to the instance.", - "region": "The resource region. If not defined, the provider region is used.", - "status": "The current status of the user.", - "connection_string": "The connection string for the user to the instance.", +func (r *userDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + s := postgresflexalpha.UserDataSourceSchema(ctx) + s.Attributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," + + "`user_id`\\\".\",", + Optional: true, + Computed: true, } - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "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(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Computed: true, - }, - "host": schema.StringAttribute{ - Description: descriptions["host"], - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "connection_string": schema.StringAttribute{ - Description: descriptions["connection_string"], - Computed: true, - }, - }, - } + resp.Schema = s } // Read refreshes the Terraform state with the latest data. @@ -190,26 +111,12 @@ func (r *userDataSource) Read( region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "user_id", userId) recordSetResp, err := r.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute() if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf( - "User with ID %q or instance with ID %q does not exist in project %q.", - userId, - instanceId, - projectId, - ), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) + handleReadError(ctx, &diags, err, projectId, instanceId, userId) resp.State.RemoveResource(ctx) return } @@ -237,26 +144,39 @@ func (r *userDataSource) Read( tflog.Info(ctx, "Postgres Flex user read") } -func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSourceModel, region string) error { - if userResp == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp - - var userId int64 - if model.UserId.ValueInt64() != 0 { - userId = model.UserId.ValueInt64() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), +// handleReadError centralizes API error handling for the Read operation. +func handleReadError( + ctx context.Context, + diags *diag.Diagnostics, + err error, + projectId, instanceId string, + userId int32, +) { + utils.LogError( + ctx, + diags, + err, + "Reading user", + fmt.Sprintf( + "User with ID %q or instance with ID %q does not exist in project %q.", + userId, + instanceId, + projectId, + ), + map[int]string{ + http.StatusBadRequest: fmt.Sprintf( + "Invalid user request parameters for project %q and instance %q.", + projectId, + instanceId, + ), + http.StatusNotFound: fmt.Sprintf( + "User, instance %q, or project %q or user %q not found.", + instanceId, + projectId, + userId, + ), + http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId), + }, ) model.UserId = types.Int64Value(userId) model.Username = types.StringPointerValue(user.Name) diff --git a/stackit/internal/services/postgresflexalpha/user/datasource_test.go b/stackit/internal/services/postgresflexalpha/user/datasource_test.go deleted file mode 100644 index 679bef85..00000000 --- a/stackit/internal/services/postgresflexalpha/user/datasource_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package postgresflexalpha - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -func TestMapDataSourceFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - "", - }, - Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, - ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Roles: &[]postgresflexalpha.UserRole{}, - Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - Status: utils.Ptr("status"), - ConnectionString: utils.Ptr("connection_string"), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapDataSourceFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 3aa5f519..0c19988f 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -2,36 +2,27 @@ package postgresflexalpha import ( "context" + _ "embed" "errors" "fmt" "math" "net/http" - "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" + postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" - postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" - - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "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/stackit/internal/conversion" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" ) // Ensure the implementation satisfies the expected interfaces. @@ -43,18 +34,8 @@ var ( ) type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.Int64 `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - ConnectionString types.String `tfsdk:"connection_string"` + postgresflexalpha.UserModel + TerraformID types.String `tfsdk:"id"` } // NewUserResource is a helper function to simplify the provider implementation. @@ -123,117 +104,30 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ tflog.Info(ctx, "Postgres Flex user client configured") } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + // Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - rolesOptions := []string{"login", "createdb", "createrole"} - - descriptions := map[string]string{ - "main": "Postgres Flex user 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`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "The name of the user.", - "roles": "Database access levels for the user. " + utils.FormatPossibleValues(rolesOptions...), - "region": "The resource region. If not defined, the provider region is used.", - "status": "The current status of the user.", - "password": "The password for the user. This is only set upon creation.", - "host": "The host of the Postgres Flex instance.", - "port": "The port of the Postgres Flex instance.", - "connection_string": "The connection string for the user to the instance.", +func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + s := postgresflexalpha.UserResourceSchema(ctx) + s.Attributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", + Optional: true, + Computed: true, } - 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(), - }, - }, - "user_id": schema.Int64Attribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Required: true, - PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - stringvalidator.OneOf(rolesOptions...), - ), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Description: descriptions["host"], - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Computed: true, - }, - "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(), - }, - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "connection_string": schema.StringAttribute{ - Description: descriptions["connection_string"], - Computed: true, - }, - }, + fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return } + + err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } // Create creates the resource and sets the initial Terraform state. @@ -277,8 +171,6 @@ func (r *userResource) Create( return } - ctx = core.LogResponse(ctx) - if userResp.Id == nil || *userResp.Id == 0 { core.LogAndAddError( ctx, @@ -288,11 +180,14 @@ func (r *userResource) Create( ) return } + model.Id = types.Int64PointerValue(userResp.Id) model.UserId = types.Int64PointerValue(userResp.Id) model.Password = types.StringPointerValue(userResp.Password) ctx = tflog.SetField(ctx, "user_id", *userResp.Id) + ctx = core.LogResponse(ctx) + exists, err := r.getUserResource(ctx, &model) if err != nil { @@ -585,7 +480,7 @@ func (r *userResource) getUserResource(ctx context.Context, model *Model) (bool, return false, fmt.Errorf("error fetching user resource: %w", err) } - if err := mapFields(userResp, model, arg.region); err != nil { + if err := mapResourceFields(userResp, model, arg.region); err != nil { return false, fmt.Errorf("error mapping user resource: %w", err) } @@ -615,13 +510,13 @@ func (r *userResource) setTFLogFields(ctx context.Context, model *Model) context ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) ctx = tflog.SetField(ctx, "instance_id", usrCtx.instanceId) - ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) ctx = tflog.SetField(ctx, "region", usrCtx.region) + ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) return ctx } -func (r *userResource) expandRoles(ctx context.Context, rolesSet types.Set, diags *diag.Diagnostics) []string { +func (r *userResource) expandRoles(ctx context.Context, rolesSet types.List, diags *diag.Diagnostics) []string { if rolesSet.IsNull() || rolesSet.IsUnknown() { return nil } @@ -629,42 +524,3 @@ func (r *userResource) expandRoles(ctx context.Context, rolesSet types.Set, diag diags.Append(rolesSet.ElementsAs(ctx, &roles, false)...) return roles } - -func toCreatePayload(model *Model, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.CreateUserRequestPayload{ - Roles: toPayloadRoles(roles), - Name: conversion.StringValueToPointer(model.Username), - }, nil -} - -func toPayloadRoles(roles *[]string) *[]postgresflex.UserRole { - var userRoles = make([]postgresflex.UserRole, 0, len(*roles)) - for _, role := range *roles { - userRoles = append(userRoles, postgresflex.UserRole(role)) - } - return &userRoles -} - -func toUpdatePayload(model *Model, roles *[]string) ( - *postgresflex.UpdateUserRequestPayload, - error, -) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.UpdateUserRequestPayload{ - Name: conversion.StringValueToPointer(model.Username), - Roles: toPayloadRoles(roles), - }, nil -} diff --git a/stackit/internal/services/postgresflexalpha/user/resource_test.go b/stackit/internal/services/postgresflexalpha/user/resource_test.go deleted file mode 100644 index e4a13482..00000000 --- a/stackit/internal/services/postgresflexalpha/user/resource_test.go +++ /dev/null @@ -1,448 +0,0 @@ -package postgresflexalpha - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" -) - -func TestMapFieldsCreate(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: utils.Ptr("username"), - ConnectionString: utils.Ptr("connection_string"), - Status: utils.Ptr("status"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: nil, - ConnectionString: nil, - Status: nil, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - 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 TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflexalpha.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(int64(1)), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "simple_values", - &postgresflexalpha.GetUserResponse{ - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - "", - }, - Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, - ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflexalpha.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflexalpha.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflexalpha.GetUserResponse{}, - 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, - UserId: tt.expected.UserId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles *[]string - expected *postgresflexalpha.CreateUserRequestPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &[]string{}, - &postgresflexalpha.CreateUserRequestPayload{ - Name: nil, - Roles: &[]postgresflexalpha.UserRole{}, - }, - true, - }, - { - "simple_values", - &Model{ - Username: types.StringValue("username"), - }, - &[]string{ - "role_1", - "role_2", - }, - &postgresflexalpha.CreateUserRequestPayload{ - Name: utils.Ptr("username"), - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - }, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - &[]string{ - "", - }, - &postgresflexalpha.CreateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{ - "", - }, - Name: nil, - }, - true, - }, - { - "nil_model", - nil, - &[]string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles *[]string - expected *postgresflexalpha.UpdateUserRequestPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &[]string{}, - &postgresflexalpha.UpdateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{}, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - }, - &[]string{ - "role_1", - "role_2", - }, - &postgresflexalpha.UpdateUserRequestPayload{ - Name: utils.Ptr("username"), - Roles: &[]postgresflexalpha.UserRole{ - "role_1", - "role_2", - }, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - &[]string{ - "", - }, - &postgresflexalpha.UpdateUserRequestPayload{ - Roles: &[]postgresflexalpha.UserRole{ - "", - }, - }, - true, - }, - { - "nil_model", - nil, - &[]string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run( - tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }, - ) - } -} -- 2.49.1 From b6d3eb3858a6c0f05178370008ce77e90037a8a4 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 16:52:32 +0100 Subject: [PATCH 19/31] feat: add documentation comments for Model and clientArg structures in resource.go --- stackit/internal/services/postgresflexalpha/user/resource.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 0c19988f..760a4fe1 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -33,6 +33,7 @@ var ( _ resource.ResourceWithModifyPlan = &userResource{} ) +// Model represents the Terraform resource state for a PostgreSQL Flex user. type Model struct { postgresflexalpha.UserModel TerraformID types.String `tfsdk:"id"` @@ -487,6 +488,7 @@ func (r *userResource) getUserResource(ctx context.Context, model *Model) (bool, return true, nil } +// clientArg holds the arguments for API calls. type clientArg struct { projectId string instanceId string @@ -516,6 +518,7 @@ func (r *userResource) setTFLogFields(ctx context.Context, model *Model) context return ctx } +// expandRoles converts a Terraform list of roles to a string slice. func (r *userResource) expandRoles(ctx context.Context, rolesSet types.List, diags *diag.Diagnostics) []string { if rolesSet.IsNull() || rolesSet.IsUnknown() { return nil -- 2.49.1 From 71dae54b8b04af403270fa6fb0def3098f71c64b Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 21:05:29 +0100 Subject: [PATCH 20/31] feat: rename Model to ResourceModel for clarity in user resource handling --- .../services/postgresflexalpha/user/mapper.go | 6 +-- .../postgresflexalpha/user/mapper_test.go | 52 +++++++++---------- .../postgresflexalpha/user/resource.go | 30 ++++++----- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/user/mapper.go b/stackit/internal/services/postgresflexalpha/user/mapper.go index 655d7d80..519f46e1 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper.go @@ -71,7 +71,7 @@ func toPayloadRoles(roles *[]string) *[]postgresflex.UserRole { } // toUpdatePayload creates an API update payload from the resource model. -func toUpdatePayload(model *Model, roles *[]string) ( +func toUpdatePayload(model *ResourceModel, roles *[]string) ( *postgresflex.UpdateUserRequestPayload, error, ) { @@ -89,7 +89,7 @@ func toUpdatePayload(model *Model, roles *[]string) ( } // toCreatePayload creates an API create payload from the resource model. -func toCreatePayload(model *Model, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { +func toCreatePayload(model *ResourceModel, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } @@ -104,7 +104,7 @@ func toCreatePayload(model *Model, roles *[]string) (*postgresflex.CreateUserReq } // mapResourceFields maps API response to the resource model, preserving existing ID. -func mapResourceFields(userResp *postgresflex.GetUserResponse, model *Model, region string) error { +func mapResourceFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } diff --git a/stackit/internal/services/postgresflexalpha/user/mapper_test.go b/stackit/internal/services/postgresflexalpha/user/mapper_test.go index 863dbbcc..3e02d25b 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper_test.go @@ -169,7 +169,7 @@ func TestMapFieldsCreate(t *testing.T) { description string input *postgresflex.GetUserResponse region string - expected Model + expected ResourceModel isValid bool }{ { @@ -178,7 +178,7 @@ func TestMapFieldsCreate(t *testing.T) { Id: utils.Ptr(int64(1)), }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -205,7 +205,7 @@ func TestMapFieldsCreate(t *testing.T) { Status: utils.Ptr("status"), }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -232,7 +232,7 @@ func TestMapFieldsCreate(t *testing.T) { Status: nil, }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -254,28 +254,28 @@ func TestMapFieldsCreate(t *testing.T) { "nil_response", nil, testRegion, - Model{}, + ResourceModel{}, false, }, { "nil_response_2", &postgresflex.GetUserResponse{}, testRegion, - Model{}, + ResourceModel{}, false, }, { "no_resource_id", &postgresflex.GetUserResponse{}, testRegion, - Model{}, + ResourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &Model{ + state := &ResourceModel{ UserModel: resource.UserModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, @@ -306,7 +306,7 @@ func TestMapFields(t *testing.T) { description string input *postgresflex.GetUserResponse region string - expected Model + expected ResourceModel isValid bool }{ { @@ -315,7 +315,7 @@ func TestMapFields(t *testing.T) { Id: utils.Ptr(int64(1)), }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ Id: types.Int64Value(1), UserId: types.Int64Value(int64(1)), @@ -347,7 +347,7 @@ func TestMapFields(t *testing.T) { Port: utils.Ptr(int64(1234)), }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ Id: types.Int64Value(1), UserId: types.Int64Value(1), @@ -382,7 +382,7 @@ func TestMapFields(t *testing.T) { Port: utils.Ptr(int64(2123456789)), }, testRegion, - Model{ + ResourceModel{ UserModel: resource.UserModel{ Id: types.Int64Value(1), UserId: types.Int64Value(1), @@ -404,28 +404,28 @@ func TestMapFields(t *testing.T) { "nil_response", nil, testRegion, - Model{}, + ResourceModel{}, false, }, { "nil_response_2", &postgresflex.GetUserResponse{}, testRegion, - Model{}, + ResourceModel{}, false, }, { "no_resource_id", &postgresflex.GetUserResponse{}, testRegion, - Model{}, + ResourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &Model{ + state := &ResourceModel{ UserModel: resource.UserModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, @@ -452,14 +452,14 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *ResourceModel inputRoles *[]string expected *postgresflex.CreateUserRequestPayload isValid bool }{ { "default_values", - &Model{}, + &ResourceModel{}, &[]string{}, &postgresflex.CreateUserRequestPayload{ Name: nil, @@ -469,7 +469,7 @@ func TestToCreatePayload(t *testing.T) { }, { "simple_values", - &Model{ + &ResourceModel{ UserModel: resource.UserModel{ Name: types.StringValue("username"), }, @@ -489,7 +489,7 @@ func TestToCreatePayload(t *testing.T) { }, { "null_fields_and_int_conversions", - &Model{ + &ResourceModel{ UserModel: resource.UserModel{ Name: types.StringNull(), }, @@ -514,7 +514,7 @@ func TestToCreatePayload(t *testing.T) { }, { "nil_roles", - &Model{}, + &ResourceModel{}, nil, nil, false, @@ -544,14 +544,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *ResourceModel inputRoles *[]string expected *postgresflex.UpdateUserRequestPayload isValid bool }{ { "default_values", - &Model{}, + &ResourceModel{}, &[]string{}, &postgresflex.UpdateUserRequestPayload{ Roles: &[]postgresflex.UserRole{}, @@ -560,7 +560,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "default_values", - &Model{ + &ResourceModel{ UserModel: resource.UserModel{ Name: types.StringValue("username"), }, @@ -580,7 +580,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "null_fields_and_int_conversions", - &Model{ + &ResourceModel{ UserModel: resource.UserModel{ Name: types.StringNull(), }, @@ -604,7 +604,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "nil_roles", - &Model{}, + &ResourceModel{}, nil, nil, false, diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 760a4fe1..06c2a1a5 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -33,8 +33,8 @@ var ( _ resource.ResourceWithModifyPlan = &userResource{} ) -// Model represents the Terraform resource state for a PostgreSQL Flex user. -type Model struct { +// ResourceModel represents the Terraform resource state for a PostgreSQL Flex user. +type ResourceModel struct { postgresflexalpha.UserModel TerraformID types.String `tfsdk:"id"` } @@ -44,7 +44,7 @@ func NewUserResource() resource.Resource { return &userResource{} } -// userResource is the resource implementation. +// userResource implements the resource handling for a PostgreSQL Flex user. type userResource struct { client *postgresflex.APIClient providerData core.ProviderData @@ -57,7 +57,7 @@ func (r *userResource) ModifyPlan( 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 @@ -67,7 +67,7 @@ func (r *userResource) ModifyPlan( return } - var planModel Model + var planModel ResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -137,7 +137,7 @@ func (r *userResource) 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() { @@ -189,6 +189,7 @@ func (r *userResource) Create( ctx = core.LogResponse(ctx) + // Verify creation exists, err := r.getUserResource(ctx, &model) if err != nil { @@ -218,7 +219,7 @@ func (r *userResource) 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() { @@ -227,6 +228,7 @@ func (r *userResource) Read( ctx = core.InitProviderContext(ctx) + // Read resource state exists, err := r.getUserResource(ctx, &model) if err != nil { @@ -256,7 +258,7 @@ func (r *userResource) Update( req resource.UpdateRequest, resp *resource.UpdateResponse, ) { // 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() { @@ -268,7 +270,7 @@ func (r *userResource) Update( arg := r.getClientArg(&model) // Retrieve values from state - var stateModel Model + var stateModel ResourceModel diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -309,6 +311,7 @@ func (r *userResource) Update( ctx = core.LogResponse(ctx) + // Verify update exists, err := r.getUserResource(ctx, &stateModel) if err != nil { @@ -339,7 +342,7 @@ func (r *userResource) 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() { @@ -365,6 +368,7 @@ func (r *userResource) Delete( ctx = core.LogResponse(ctx) + // Verify deletion exists, err := r.getUserResource(ctx, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) @@ -459,7 +463,7 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri // getUserResource refreshes the resource state by calling the API and mapping the response to the model. // Returns true if the resource state was successfully refreshed, false if the resource does not exist. -func (r *userResource) getUserResource(ctx context.Context, model *Model) (bool, error) { +func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel) (bool, error) { ctx = r.setTFLogFields(ctx, model) arg := r.getClientArg(model) @@ -497,7 +501,7 @@ type clientArg struct { } // getClientArg constructs client arguments from the model. -func (r *userResource) getClientArg(model *Model) *clientArg { +func (r *userResource) getClientArg(model *ResourceModel) *clientArg { return &clientArg{ projectId: model.ProjectId.ValueString(), instanceId: model.InstanceId.ValueString(), @@ -507,7 +511,7 @@ func (r *userResource) getClientArg(model *Model) *clientArg { } // setTFLogFields adds relevant fields to the context for terraform logging purposes. -func (r *userResource) setTFLogFields(ctx context.Context, model *Model) context.Context { +func (r *userResource) setTFLogFields(ctx context.Context, model *ResourceModel) context.Context { usrCtx := r.getClientArg(model) ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) -- 2.49.1 From e2f737084955f3c709f8fb458f07c685de330dc2 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 21:13:48 +0100 Subject: [PATCH 21/31] feat: implement IdentitySchema for user resource to define unique identification fields --- .../postgresflexalpha/user/resource.go | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 06c2a1a5..c0934786 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" @@ -31,6 +32,7 @@ var ( _ resource.ResourceWithConfigure = &userResource{} _ resource.ResourceWithImportState = &userResource{} _ resource.ResourceWithModifyPlan = &userResource{} + _ resource.ResourceWithIdentity = &userResource{} ) // ResourceModel represents the Terraform resource state for a PostgreSQL Flex user. @@ -387,6 +389,30 @@ 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, + }, + }, + } +} + // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,zone_id,record_set_id func (r *userResource) ImportState( -- 2.49.1 From a46e26b69b3c60917654e82be8172c7af32e5a4b Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 23:29:27 +0100 Subject: [PATCH 22/31] feat: enhance database resource model with project, region, and instance attributes --- .../postgresflexalpha/database/resource.go | 146 ++++++++++++++---- 1 file changed, 112 insertions(+), 34 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 169c3e99..f27fbc4e 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -14,9 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" @@ -25,24 +22,33 @@ import ( "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" ) -// 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{} + // Define errors errDatabaseNotFound = errors.New("database not found") + + // Error message constants + extractErrorSummary = "extracting failed" + extractErrorMessage = "Extracting identity data: %v" ) // ResourceModel describes the resource data model. type ResourceModel struct { postgresflexalpha2.DatabaseModel - TerraformID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + InstanceID types.String `tfsdk:"instance_id"` DatabaseID types.Int64 `tfsdk:"database_id"` + TerraformID types.String `tfsdk:"id"` } // DatabaseResourceIdentityModel describes the resource's identity attributes. @@ -86,7 +92,7 @@ func (r *databaseResource) ModifyPlan( return } - //TODO utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) if resp.Diagnostics.HasError() { return } @@ -128,19 +134,33 @@ var modifiersFileByte []byte // Schema defines the schema for the resource. func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha2.DatabaseResourceSchema(ctx) + + s.Attributes["project_id"] = schema.StringAttribute{ + Description: "STACKIT project ID to which the instance is associated.", + MarkdownDescription: "STACKIT project ID to which the instance is associated.", + Required: true, + } + s.Attributes["instance_id"] = schema.StringAttribute{ + Description: "ID of the PostgresFlex instance.", + MarkdownDescription: "ID of the PostgresFlex instance.", + Required: true, + } + s.Attributes["region"] = schema.StringAttribute{ + Description: "Region of the PostgresFlex instance.", + MarkdownDescription: "Region of the PostgresFlex instance.", + Optional: true, + Computed: true, + } + s.Attributes["database_id"] = schema.Int64Attribute{ + Description: "The ID of the database.", + Computed: true, + } + s.Attributes["id"] = schema.StringAttribute{ Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", Optional: true, Computed: true, } - s.Attributes["database_id"] = schema.Int64Attribute{ - Description: "ID of the database.", - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - } fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) if err != nil { @@ -300,10 +320,16 @@ func (r *databaseResource) Read( ctx = core.InitProviderContext(ctx) - projectId := identityData.ProjectID.ValueString() - instanceId := identityData.InstanceID.ValueString() - databaseId := model.DatabaseID.ValueInt64() - region := r.providerData.GetRegionWithOverride(identityData.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, "region", region) @@ -365,11 +391,16 @@ func (r *databaseResource) Update( ctx = core.InitProviderContext(ctx) - projectId := identityData.ProjectID.ValueString() - instanceId := identityData.InstanceID.ValueString() - region := r.providerData.GetRegionWithOverride(identityData.Region) + projectId, instanceId, region, databaseId64, errExt := r.extractIdentityData(model, identityData) + if errExt != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + extractErrorSummary, + fmt.Sprintf(extractErrorMessage, errExt), + ) + } - databaseId64 := model.DatabaseID.ValueInt64() // database id if databaseId64 > math.MaxInt32 { core.LogAndAddError(ctx, &resp.Diagnostics, "Error in type conversion", "int value too large (databaseId)") return @@ -463,10 +494,15 @@ func (r *databaseResource) Delete( ctx = core.InitProviderContext(ctx) - projectId := identityData.ProjectID.ValueString() - instanceId := identityData.InstanceID.ValueString() - region := r.providerData.GetRegionWithOverride(identityData.Region) - databaseId64 := model.DatabaseID.ValueInt64() //database id + 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)") @@ -525,13 +561,12 @@ func (r *databaseResource) ImportState( 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)...) - //TODO: Investigate if this logic is still required. - //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.", - //) + 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.", + ) var identityData DatabaseResourceIdentityModel identityData.ProjectID = types.StringValue(idParts[0]) @@ -539,7 +574,7 @@ func (r *databaseResource) ImportState( identityData.InstanceID = types.StringValue(idParts[2]) identityData.DatabaseID = types.Int64Value(databaseId) - resp.Diagnostics.Append(req.Identity.Set(ctx, &identityData)...) + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) if resp.Diagnostics.HasError() { return @@ -548,3 +583,46 @@ func (r *databaseResource) ImportState( tflog.Info(ctx, "Postgres Flex instance 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 +} -- 2.49.1 From 5ec7e54d41c8ad859f4fc62f50dddf7337ec4c47 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 23:29:34 +0100 Subject: [PATCH 23/31] feat: add validation for project_id and instance_id attributes in datasource schema --- .../postgresflexalpha/database/datasource.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 3e526242..1c4de9bc 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -7,10 +7,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" 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/validate" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -83,11 +85,19 @@ func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequ Description: "STACKIT project ID to which the instance is associated.", MarkdownDescription: "STACKIT project ID to which the instance is associated.", Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, } s.Attributes["instance_id"] = schema.StringAttribute{ Description: "ID of the PostgresFlex instance.", MarkdownDescription: "ID of the PostgresFlex instance.", Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, } s.Attributes["region"] = schema.StringAttribute{ Description: "Region of the PostgresFlex instance.", @@ -96,13 +106,11 @@ func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequ } s.Attributes["database_id"] = schema.Int64Attribute{ Description: "The ID of the database.", - Optional: true, - Computed: true, + Required: true, } s.Attributes["id"] = schema.StringAttribute{ Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`," + "`database_id`\\\".\",", - Optional: true, Computed: true, } -- 2.49.1 From d5d0caf5c7ce78ce742a680ce0db01df59771cca Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Thu, 5 Feb 2026 23:29:46 +0100 Subject: [PATCH 24/31] feat: enhance user resource with identity data extraction and logging --- .../postgresflexalpha/user/resource.go | 213 +++++++++++++++--- 1 file changed, 181 insertions(+), 32 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index c0934786..e55c5dee 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "net/http" + "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -26,13 +27,17 @@ import ( "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" ) -// Ensure the implementation satisfies the expected interfaces. var ( + // Ensure the implementation satisfies the expected interfaces. _ resource.Resource = &userResource{} _ resource.ResourceWithConfigure = &userResource{} _ resource.ResourceWithImportState = &userResource{} _ resource.ResourceWithModifyPlan = &userResource{} _ resource.ResourceWithIdentity = &userResource{} + + // Error message constants + extractErrorSummary = "extracting failed" + extractErrorMessage = "Extracting identity data: %v" ) // ResourceModel represents the Terraform resource state for a PostgreSQL Flex user. @@ -41,6 +46,14 @@ type ResourceModel struct { TerraformID types.String `tfsdk:"id"` } +// 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:"database_id"` +} + // NewUserResource is a helper function to simplify the provider implementation. func NewUserResource() resource.Resource { return &userResource{} @@ -146,9 +159,26 @@ func (r *userResource) Create( return } + // Read identity data + var identityData UserResourceIdentityModel + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + if resp.Diagnostics.HasError() { + return + } + ctx = core.InitProviderContext(ctx) - ctx = r.setTFLogFields(ctx, &model) - arg := r.getClientArg(&model) + + arg, errExt := r.extractIdentityData(model, identityData) + if errExt != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + extractErrorSummary, + fmt.Sprintf(extractErrorMessage, errExt), + ) + } + + ctx = r.setTFLogFields(ctx, arg) var roles = r.expandRoles(ctx, model.Roles, &resp.Diagnostics) if resp.Diagnostics.HasError() { @@ -192,7 +222,7 @@ func (r *userResource) Create( ctx = core.LogResponse(ctx) // Verify creation - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) @@ -228,10 +258,31 @@ func (r *userResource) Read( 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), + ) + } + + ctx = r.setTFLogFields(ctx, arg) + ctx = core.InitProviderContext(ctx) // Read resource state - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) @@ -267,9 +318,27 @@ func (r *userResource) Update( 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), + ) + } + + ctx = r.setTFLogFields(ctx, arg) ctx = core.InitProviderContext(ctx) - ctx = r.setTFLogFields(ctx, &model) - arg := r.getClientArg(&model) // Retrieve values from state var stateModel ResourceModel @@ -314,7 +383,7 @@ func (r *userResource) Update( ctx = core.LogResponse(ctx) // Verify update - exists, err := r.getUserResource(ctx, &stateModel) + exists, err := r.getUserResource(ctx, &stateModel, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err)) @@ -350,10 +419,27 @@ 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) - ctx = r.setTFLogFields(ctx, &model) - arg := r.getClientArg(&model) + + arg, errExt := r.extractIdentityData(model, identityData) + if errExt != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + extractErrorSummary, + fmt.Sprintf(extractErrorMessage, errExt), + ) + } + + ctx = r.setTFLogFields(ctx, arg) + ctx = core.InitProviderContext(ctx) userId64 := arg.userId if userId64 > math.MaxInt32 { @@ -371,7 +457,7 @@ func (r *userResource) Delete( ctx = core.LogResponse(ctx) // Verify deletion - exists, err := r.getUserResource(ctx, &model) + exists, err := r.getUserResource(ctx, &model, arg) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) return @@ -433,16 +519,42 @@ func (r *userResource) ImportState( return } + userId, err := strconv.ParseInt(idParts[3], 10, 64) + if err != nil { + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Invalid userId 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"), idParts[3])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...) + core.LogAndAddWarning( ctx, &resp.Diagnostics, "postgresflexalpha user imported with empty password and empty uri", "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", ) + + var identityData UserResourceIdentityModel + identityData.ProjectID = types.StringValue(idParts[0]) + identityData.Region = types.StringValue(idParts[1]) + identityData.InstanceID = types.StringValue(idParts[2]) + identityData.UserID = types.Int64Value(userId) + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "Postgres Flex instance state imported") tflog.Info(ctx, "postgresflexalpha user state imported") } @@ -489,15 +601,12 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri // getUserResource refreshes the resource state by calling the API and mapping the response to the model. // Returns true if the resource state was successfully refreshed, false if the resource does not exist. -func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel) (bool, error) { - ctx = r.setTFLogFields(ctx, model) - arg := r.getClientArg(model) +func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel, arg *clientArg) (bool, error) { - userId64 := arg.userId - if userId64 > math.MaxInt32 { + if arg.userId > math.MaxInt32 { return false, errors.New("error in type conversion: int value too large (userId)") } - userId := int32(userId64) + userId := int32(arg.userId) // API Call userResp, err := r.client.GetUserRequest(ctx, arg.projectId, arg.region, arg.instanceId, userId).Execute() @@ -526,24 +635,64 @@ type clientArg struct { userId int64 } -// getClientArg constructs client arguments from the model. -func (r *userResource) getClientArg(model *ResourceModel) *clientArg { - return &clientArg{ - projectId: model.ProjectId.ValueString(), - instanceId: model.InstanceId.ValueString(), - region: r.providerData.GetRegionWithOverride(model.Region), - userId: model.UserId.ValueInt64(), +// 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, model *ResourceModel) context.Context { - usrCtx := r.getClientArg(model) - - ctx = tflog.SetField(ctx, "project_id", usrCtx.projectId) - ctx = tflog.SetField(ctx, "instance_id", usrCtx.instanceId) - ctx = tflog.SetField(ctx, "region", usrCtx.region) - ctx = tflog.SetField(ctx, "user_id", usrCtx.userId) +func (r *userResource) setTFLogFields(ctx context.Context, arg *clientArg) context.Context { + ctx = tflog.SetField(ctx, "project_id", arg.projectId) + ctx = tflog.SetField(ctx, "instance_id", arg.instanceId) + ctx = tflog.SetField(ctx, "region", arg.region) + ctx = tflog.SetField(ctx, "user_id", arg.userId) return ctx } -- 2.49.1 From c4891b770d58412770d53eee84cfe4c46bf73ae6 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Fri, 6 Feb 2026 13:14:35 +0100 Subject: [PATCH 25/31] feat: add database data source schema and model with required attributes --- .../database_data_source_fix.go | 532 ++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go new file mode 100644 index 00000000..b2d62433 --- /dev/null +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go @@ -0,0 +1,532 @@ +// Code generated by terraform-plugin-framework-generator DO NOT EDIT. + +package postgresflexalpha + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" +) + +func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + Description: "The id of the database.", + MarkdownDescription: "The id of the database.", + }, + "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.", + }, + "database_id": schema.Int64Attribute{ + Required: 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.", + }, + "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 { + Id types.Int64 `tfsdk:"tf_original_api_id"` + Name types.String `tfsdk:"name"` + Owner types.String `tfsdk:"owner"` + DatabaseId types.Int64 `tfsdk:"database_id"` + InstanceId types.String `tfsdk:"instance_id"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` +} + +var _ basetypes.ObjectTypable = DatabaseType{} + +type DatabaseType struct { + basetypes.ObjectType +} + +func (t DatabaseType) Equal(o attr.Type) bool { + other, ok := o.(DatabaseType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +func (t DatabaseType) String() string { + return "DatabaseType" +} + +func (t DatabaseType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) ( + basetypes.ObjectValuable, + diag.Diagnostics, +) { + var diags diag.Diagnostics + + attributes := in.Attributes() + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`, + ) + + return nil, diags + } + + idVal, ok := idAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute), + ) + } + + nameAttribute, ok := attributes["name"] + + if !ok { + diags.AddError( + "Attribute Missing", + `name is missing from object`, + ) + + return nil, diags + } + + nameVal, ok := nameAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute), + ) + } + + ownerAttribute, ok := attributes["owner"] + + if !ok { + diags.AddError( + "Attribute Missing", + `owner is missing from object`, + ) + + return nil, diags + } + + ownerVal, ok := ownerAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute), + ) + } + + if diags.HasError() { + return nil, diags + } + + return DatabaseValue{ + Id: idVal, + Name: nameVal, + Owner: ownerVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewDatabaseValueNull() DatabaseValue { + return DatabaseValue{ + state: attr.ValueStateNull, + } +} + +func NewDatabaseValueUnknown() DatabaseValue { + return DatabaseValue{ + state: attr.ValueStateUnknown, + } +} + +func NewDatabaseValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) ( + DatabaseValue, + diag.Diagnostics, +) { + var diags diag.Diagnostics + + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 + ctx := context.Background() + + for name, attributeType := range attributeTypes { + attribute, ok := attributes[name] + + if !ok { + diags.AddError( + "Missing DatabaseValue Attribute Value", + "While creating a DatabaseValue value, a missing attribute value was detected. "+ + "A DatabaseValue must contain values for all attributes, even if null or unknown. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("DatabaseValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), + ) + + continue + } + + if !attributeType.Equal(attribute.Type(ctx)) { + diags.AddError( + "Invalid DatabaseValue Attribute Type", + "While creating a DatabaseValue value, an invalid attribute value was detected. "+ + "A DatabaseValue must use a matching attribute type for the value. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("DatabaseValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ + fmt.Sprintf("DatabaseValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), + ) + } + } + + for name := range attributes { + _, ok := attributeTypes[name] + + if !ok { + diags.AddError( + "Extra DatabaseValue Attribute Value", + "While creating a DatabaseValue value, an extra attribute value was detected. "+ + "A DatabaseValue must not contain values beyond the expected attribute types. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Extra DatabaseValue Attribute Name: %s", name), + ) + } + } + + if diags.HasError() { + return NewDatabaseValueUnknown(), diags + } + + idAttribute, ok := attributes["id"] + + if !ok { + diags.AddError( + "Attribute Missing", + `id is missing from object`, + ) + + return NewDatabaseValueUnknown(), diags + } + + idVal, ok := idAttribute.(basetypes.Int64Value) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute), + ) + } + + nameAttribute, ok := attributes["name"] + + if !ok { + diags.AddError( + "Attribute Missing", + `name is missing from object`, + ) + + return NewDatabaseValueUnknown(), diags + } + + nameVal, ok := nameAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute), + ) + } + + ownerAttribute, ok := attributes["owner"] + + if !ok { + diags.AddError( + "Attribute Missing", + `owner is missing from object`, + ) + + return NewDatabaseValueUnknown(), diags + } + + ownerVal, ok := ownerAttribute.(basetypes.StringValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute), + ) + } + + if diags.HasError() { + return NewDatabaseValueUnknown(), diags + } + + return DatabaseValue{ + Id: idVal, + Name: nameVal, + Owner: ownerVal, + state: attr.ValueStateKnown, + }, diags +} + +func NewDatabaseValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) DatabaseValue { + object, diags := NewDatabaseValue(attributeTypes, attributes) + + if diags.HasError() { + // This could potentially be added to the diag package. + diagsStrings := make([]string, 0, len(diags)) + + for _, diagnostic := range diags { + diagsStrings = append( + diagsStrings, fmt.Sprintf( + "%s | %s | %s", + diagnostic.Severity(), + diagnostic.Summary(), + diagnostic.Detail(), + ), + ) + } + + panic("NewDatabaseValueMust received error(s): " + strings.Join(diagsStrings, "\n")) + } + + return object +} + +func (t DatabaseType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + if in.Type() == nil { + return NewDatabaseValueNull(), nil + } + + if !in.Type().Equal(t.TerraformType(ctx)) { + return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) + } + + if !in.IsKnown() { + return NewDatabaseValueUnknown(), nil + } + + if in.IsNull() { + return NewDatabaseValueNull(), nil + } + + attributes := map[string]attr.Value{} + + val := map[string]tftypes.Value{} + + err := in.As(&val) + + if err != nil { + return nil, err + } + + for k, v := range val { + a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) + + if err != nil { + return nil, err + } + + attributes[k] = a + } + + return NewDatabaseValueMust(DatabaseValue{}.AttributeTypes(ctx), attributes), nil +} + +func (t DatabaseType) ValueType(ctx context.Context) attr.Value { + return DatabaseValue{} +} + +var _ basetypes.ObjectValuable = DatabaseValue{} + +type DatabaseValue struct { + Id basetypes.Int64Value `tfsdk:"id"` + Name basetypes.StringValue `tfsdk:"name"` + Owner basetypes.StringValue `tfsdk:"owner"` + state attr.ValueState +} + +func (v DatabaseValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { + attrTypes := make(map[string]tftypes.Type, 3) + + var val tftypes.Value + var err error + + attrTypes["id"] = basetypes.Int64Type{}.TerraformType(ctx) + attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["owner"] = basetypes.StringType{}.TerraformType(ctx) + + objectType := tftypes.Object{AttributeTypes: attrTypes} + + switch v.state { + case attr.ValueStateKnown: + vals := make(map[string]tftypes.Value, 3) + + val, err = v.Id.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["id"] = val + + val, err = v.Name.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["name"] = val + + val, err = v.Owner.ToTerraformValue(ctx) + + if err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + vals["owner"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } + + return tftypes.NewValue(objectType, vals), nil + case attr.ValueStateNull: + return tftypes.NewValue(objectType, nil), nil + case attr.ValueStateUnknown: + return tftypes.NewValue(objectType, tftypes.UnknownValue), nil + default: + panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) + } +} + +func (v DatabaseValue) IsNull() bool { + return v.state == attr.ValueStateNull +} + +func (v DatabaseValue) IsUnknown() bool { + return v.state == attr.ValueStateUnknown +} + +func (v DatabaseValue) String() string { + return "DatabaseValue" +} + +func (v DatabaseValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { + var diags diag.Diagnostics + + attributeTypes := map[string]attr.Type{ + "id": basetypes.Int64Type{}, + "name": basetypes.StringType{}, + "owner": basetypes.StringType{}, + } + + if v.IsNull() { + return types.ObjectNull(attributeTypes), diags + } + + if v.IsUnknown() { + return types.ObjectUnknown(attributeTypes), diags + } + + objVal, diags := types.ObjectValue( + attributeTypes, + map[string]attr.Value{ + "id": v.Id, + "name": v.Name, + "owner": v.Owner, + }, + ) + + return objVal, diags +} + +func (v DatabaseValue) Equal(o attr.Value) bool { + other, ok := o.(DatabaseValue) + + if !ok { + return false + } + + if v.state != other.state { + return false + } + + if v.state != attr.ValueStateKnown { + return true + } + + if !v.Id.Equal(other.Id) { + return false + } + + if !v.Name.Equal(other.Name) { + return false + } + + if !v.Owner.Equal(other.Owner) { + return false + } + + return true +} + +func (v DatabaseValue) Type(ctx context.Context) attr.Type { + return DatabaseType{ + basetypes.ObjectType{ + AttrTypes: v.AttributeTypes(ctx), + }, + } +} + +func (v DatabaseValue) AttributeTypes(ctx context.Context) map[string]attr.Type { + return map[string]attr.Type{ + "id": basetypes.Int64Type{}, + "name": basetypes.StringType{}, + "owner": basetypes.StringType{}, + } +} -- 2.49.1 From 1284bcc8a356f248e08a78ccd7ee73b07a9e241c Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Mon, 9 Feb 2026 09:34:55 +0100 Subject: [PATCH 26/31] feat: enhance database resource model with additional attributes and refactor data source schema --- .../postgresflexalpha/database/datasource.go | 46 +++---------------- .../postgresflexalpha/database/mapper.go | 18 ++++++-- .../postgresflexalpha/database/mapper_test.go | 42 +++++++++-------- .../postgresflexalpha/database/resource.go | 37 ++++++++------- .../resources_gen/database_resource_gen.go | 3 ++ ...rce_gen.go => database_data_source_fix.go} | 0 6 files changed, 66 insertions(+), 80 deletions(-) rename stackit/internal/services/sqlserverflexalpha/database/datasources_gen/{database_data_source_gen.go => database_data_source_fix.go} (100%) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index 1c4de9bc..e836bcde 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -5,18 +5,15 @@ import ( "fmt" "net/http" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/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/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "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/pkg_gen/postgresflexalpha" @@ -25,10 +22,6 @@ import ( // DataSourceModel maps the data source schema data. type DataSourceModel struct { postgresflexalpha2.DatabaseModel - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Region types.String `tfsdk:"region"` - DatabaseID types.Int64 `tfsdk:"database_id"` TerraformID types.String `tfsdk:"id"` } @@ -80,34 +73,7 @@ func (r *databaseDataSource) Configure( // Schema defines the schema for the data source. func (r *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - s := postgresflexalpha2.DatabaseResourceSchema(ctx) - s.Attributes["project_id"] = schema.StringAttribute{ - Description: "STACKIT project ID to which the instance is associated.", - MarkdownDescription: "STACKIT project ID to which the instance is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - } - s.Attributes["instance_id"] = schema.StringAttribute{ - Description: "ID of the PostgresFlex instance.", - MarkdownDescription: "ID of the PostgresFlex instance.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - } - s.Attributes["region"] = schema.StringAttribute{ - Description: "Region of the PostgresFlex instance.", - MarkdownDescription: "Region of the PostgresFlex instance.", - Optional: true, - } - s.Attributes["database_id"] = schema.Int64Attribute{ - Description: "The ID of the database.", - Required: true, - } + 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`\\\".\",", @@ -179,7 +145,7 @@ func (r *databaseDataSource) getDatabaseByNameOrID( projectId, region, instanceId string, diags *diag.Diagnostics, ) (*postgresflexalpha.ListDatabase, error) { - isIdSet := !model.DatabaseID.IsNull() && !model.DatabaseID.IsUnknown() + isIdSet := !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() isNameSet := !model.Name.IsNull() && !model.Name.IsUnknown() if (isIdSet && isNameSet) || (!isIdSet && !isNameSet) { @@ -191,7 +157,7 @@ func (r *databaseDataSource) getDatabaseByNameOrID( } if isIdSet { - databaseId := model.DatabaseID.ValueInt64() + databaseId := model.DatabaseId.ValueInt64() ctx = tflog.SetField(ctx, "database_id", databaseId) return getDatabaseById(ctx, r.client, projectId, region, instanceId, databaseId) } diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 2fa33645..61707ade 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -2,9 +2,11 @@ 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. @@ -24,8 +26,8 @@ func mapFields( } var databaseId int64 - if model.Id.ValueInt64() != 0 { - databaseId = model.Id.ValueInt64() + if model.DatabaseId.ValueInt64() != 0 { + databaseId = model.DatabaseId.ValueInt64() } else if source.Id != nil { databaseId = *source.Id } else { @@ -33,10 +35,18 @@ func mapFields( } model.Id = types.Int64Value(databaseId) - model.DatabaseID = 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 } @@ -63,7 +73,7 @@ func mapResourceFields(source *postgresflexalpha.ListDatabase, model *ResourceMo } model.Id = types.Int64Value(databaseId) - model.DatabaseID = types.Int64Value(databaseId) + model.DatabaseId = types.Int64Value(databaseId) model.Name = types.StringValue(source.GetName()) model.Owner = types.StringPointerValue(cleanString(source.Owner)) return nil diff --git a/stackit/internal/services/postgresflexalpha/database/mapper_test.go b/stackit/internal/services/postgresflexalpha/database/mapper_test.go index ead6b1e3..57024a5d 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper_test.go @@ -8,7 +8,6 @@ import ( "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" - resource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/database/resources_gen" ) func TestMapFields(t *testing.T) { @@ -41,12 +40,15 @@ func TestMapFields(t *testing.T) { expected: expected{ model: &DataSourceModel{ DatabaseModel: datasource.DatabaseModel{ - Id: types.Int64Value(1), - Name: types.StringValue("my-db"), - Owner: types.StringValue("my-owner"), + 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"), }, - Region: types.StringValue("eu01"), - DatabaseID: types.Int64Value(1), + TerraformID: types.StringValue("my-project,eu01,my-instance,1"), }, }, }, @@ -59,7 +61,9 @@ func TestMapFields(t *testing.T) { }, model: &DataSourceModel{ DatabaseModel: datasource.DatabaseModel{ - Id: types.Int64Value(1), + Id: types.Int64Value(1), + ProjectId: types.StringValue("my-project"), + InstanceId: types.StringValue("my-instance"), }, }, region: "eu01", @@ -69,10 +73,12 @@ func TestMapFields(t *testing.T) { DatabaseModel: datasource.DatabaseModel{ Id: types.Int64Value(1), Name: types.StringValue("my-db"), - Owner: types.StringNull(), + Owner: types.StringNull(), DatabaseId: types.Int64Value(1), + Region: types.StringValue("eu01"), + InstanceId: types.StringValue("my-instance"), + ProjectId: types.StringValue("my-project"), }, - DatabaseID: types.Int64Value(1), - Region: types.StringValue("eu01"), + TerraformID: types.StringValue("my-project,eu01,my-instance,1"), }, }, }, @@ -146,12 +152,10 @@ func TestMapResourceFields(t *testing.T) { }, expected: expected{ model: &ResourceModel{ - DatabaseModel: resource.DatabaseModel{ - Id: types.Int64Value(1), - Name: types.StringValue("my-db"), - Owner: types.StringValue("my-owner"), - }, - DatabaseID: types.Int64Value(1), + Id: types.Int64Value(1), + Name: types.StringValue("my-db"), + Owner: types.StringValue("my-owner"), + DatabaseId: types.Int64Value(1), }, }, }, @@ -200,10 +204,8 @@ func TestToCreatePayload(t *testing.T) { name: "should convert model to payload", given: given{ model: &ResourceModel{ - DatabaseModel: resource.DatabaseModel{ - Name: types.StringValue("my-db"), - Owner: types.StringValue("my-owner"), - }, + Name: types.StringValue("my-db"), + Owner: types.StringValue("my-owner"), }, }, expected: expected{ diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index f27fbc4e..3392f39e 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -42,14 +42,7 @@ var ( ) // ResourceModel describes the resource data model. -type ResourceModel struct { - postgresflexalpha2.DatabaseModel - ProjectID types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - InstanceID types.String `tfsdk:"instance_id"` - DatabaseID types.Int64 `tfsdk:"database_id"` - TerraformID types.String `tfsdk:"id"` -} +type ResourceModel = postgresflexalpha2.DatabaseModel // DatabaseResourceIdentityModel describes the resource's identity attributes. type DatabaseResourceIdentityModel struct { @@ -289,9 +282,21 @@ func (r *databaseResource) Create( ) return } + + // Write identity attributes to state + identityData.ProjectID = types.StringValue(projectId) + identityData.Region = types.StringValue(region) + identityData.InstanceID = types.StringValue(instanceId) + identityData.DatabaseID = types.Int64Value(databaseId) + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + + 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 } @@ -589,8 +594,8 @@ 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() + 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") @@ -598,8 +603,8 @@ func (r *databaseResource) extractIdentityData( databaseId = identity.DatabaseID.ValueInt64() } - if !model.ProjectID.IsNull() && !model.ProjectID.IsUnknown() { - projectId = model.ProjectID.ValueString() + 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") @@ -616,8 +621,8 @@ func (r *databaseResource) extractIdentityData( region = r.providerData.GetRegionWithOverride(identity.Region) } - if !model.InstanceID.IsNull() && !model.InstanceID.IsUnknown() { - instanceId = model.InstanceID.ValueString() + 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") diff --git a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go index 6affc956..8dff24a0 100644 --- a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go @@ -7,6 +7,9 @@ import ( "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/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "strings" "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) diff --git a/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_fix.go similarity index 100% rename from stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go rename to stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_fix.go -- 2.49.1 From f0e7c19cdfac82f1e78d521b799fa178e2cad7be Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Mon, 9 Feb 2026 15:38:54 +0100 Subject: [PATCH 27/31] feat: add project_id and region attributes to database data source schema and refactor user model mapping --- .../database_data_source_fix.go | 532 ------------------ .../database_data_source_gen.go | 22 +- .../resources_gen/database_resource_gen.go | 3 - .../postgresflexalpha/user/datasource.go | 19 - .../services/postgresflexalpha/user/mapper.go | 6 - .../postgresflexalpha/user/mapper_test.go | 88 +-- .../postgresflexalpha/user/resource.go | 11 +- ...rce_fix.go => database_data_source_gen.go} | 0 8 files changed, 51 insertions(+), 630 deletions(-) delete mode 100644 stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go rename stackit/internal/services/sqlserverflexalpha/database/datasources_gen/{database_data_source_fix.go => database_data_source_gen.go} (100%) diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go deleted file mode 100644 index b2d62433..00000000 --- a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_fix.go +++ /dev/null @@ -1,532 +0,0 @@ -// Code generated by terraform-plugin-framework-generator DO NOT EDIT. - -package postgresflexalpha - -import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" -) - -func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { - return schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.Int64Attribute{ - Computed: true, - Description: "The id of the database.", - MarkdownDescription: "The id of the database.", - }, - "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.", - }, - "database_id": schema.Int64Attribute{ - Required: 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.", - }, - "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 { - Id types.Int64 `tfsdk:"tf_original_api_id"` - Name types.String `tfsdk:"name"` - Owner types.String `tfsdk:"owner"` - DatabaseId types.Int64 `tfsdk:"database_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` -} - -var _ basetypes.ObjectTypable = DatabaseType{} - -type DatabaseType struct { - basetypes.ObjectType -} - -func (t DatabaseType) Equal(o attr.Type) bool { - other, ok := o.(DatabaseType) - - if !ok { - return false - } - - return t.ObjectType.Equal(other.ObjectType) -} - -func (t DatabaseType) String() string { - return "DatabaseType" -} - -func (t DatabaseType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) ( - basetypes.ObjectValuable, - diag.Diagnostics, -) { - var diags diag.Diagnostics - - attributes := in.Attributes() - - idAttribute, ok := attributes["id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `id is missing from object`, - ) - - return nil, diags - } - - idVal, ok := idAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute), - ) - } - - nameAttribute, ok := attributes["name"] - - if !ok { - diags.AddError( - "Attribute Missing", - `name is missing from object`, - ) - - return nil, diags - } - - nameVal, ok := nameAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute), - ) - } - - ownerAttribute, ok := attributes["owner"] - - if !ok { - diags.AddError( - "Attribute Missing", - `owner is missing from object`, - ) - - return nil, diags - } - - ownerVal, ok := ownerAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute), - ) - } - - if diags.HasError() { - return nil, diags - } - - return DatabaseValue{ - Id: idVal, - Name: nameVal, - Owner: ownerVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewDatabaseValueNull() DatabaseValue { - return DatabaseValue{ - state: attr.ValueStateNull, - } -} - -func NewDatabaseValueUnknown() DatabaseValue { - return DatabaseValue{ - state: attr.ValueStateUnknown, - } -} - -func NewDatabaseValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) ( - DatabaseValue, - diag.Diagnostics, -) { - var diags diag.Diagnostics - - // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521 - ctx := context.Background() - - for name, attributeType := range attributeTypes { - attribute, ok := attributes[name] - - if !ok { - diags.AddError( - "Missing DatabaseValue Attribute Value", - "While creating a DatabaseValue value, a missing attribute value was detected. "+ - "A DatabaseValue must contain values for all attributes, even if null or unknown. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("DatabaseValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()), - ) - - continue - } - - if !attributeType.Equal(attribute.Type(ctx)) { - diags.AddError( - "Invalid DatabaseValue Attribute Type", - "While creating a DatabaseValue value, an invalid attribute value was detected. "+ - "A DatabaseValue must use a matching attribute type for the value. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("DatabaseValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+ - fmt.Sprintf("DatabaseValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)), - ) - } - } - - for name := range attributes { - _, ok := attributeTypes[name] - - if !ok { - diags.AddError( - "Extra DatabaseValue Attribute Value", - "While creating a DatabaseValue value, an extra attribute value was detected. "+ - "A DatabaseValue must not contain values beyond the expected attribute types. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Extra DatabaseValue Attribute Name: %s", name), - ) - } - } - - if diags.HasError() { - return NewDatabaseValueUnknown(), diags - } - - idAttribute, ok := attributes["id"] - - if !ok { - diags.AddError( - "Attribute Missing", - `id is missing from object`, - ) - - return NewDatabaseValueUnknown(), diags - } - - idVal, ok := idAttribute.(basetypes.Int64Value) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`id expected to be basetypes.Int64Value, was: %T`, idAttribute), - ) - } - - nameAttribute, ok := attributes["name"] - - if !ok { - diags.AddError( - "Attribute Missing", - `name is missing from object`, - ) - - return NewDatabaseValueUnknown(), diags - } - - nameVal, ok := nameAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`name expected to be basetypes.StringValue, was: %T`, nameAttribute), - ) - } - - ownerAttribute, ok := attributes["owner"] - - if !ok { - diags.AddError( - "Attribute Missing", - `owner is missing from object`, - ) - - return NewDatabaseValueUnknown(), diags - } - - ownerVal, ok := ownerAttribute.(basetypes.StringValue) - - if !ok { - diags.AddError( - "Attribute Wrong Type", - fmt.Sprintf(`owner expected to be basetypes.StringValue, was: %T`, ownerAttribute), - ) - } - - if diags.HasError() { - return NewDatabaseValueUnknown(), diags - } - - return DatabaseValue{ - Id: idVal, - Name: nameVal, - Owner: ownerVal, - state: attr.ValueStateKnown, - }, diags -} - -func NewDatabaseValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) DatabaseValue { - object, diags := NewDatabaseValue(attributeTypes, attributes) - - if diags.HasError() { - // This could potentially be added to the diag package. - diagsStrings := make([]string, 0, len(diags)) - - for _, diagnostic := range diags { - diagsStrings = append( - diagsStrings, fmt.Sprintf( - "%s | %s | %s", - diagnostic.Severity(), - diagnostic.Summary(), - diagnostic.Detail(), - ), - ) - } - - panic("NewDatabaseValueMust received error(s): " + strings.Join(diagsStrings, "\n")) - } - - return object -} - -func (t DatabaseType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { - if in.Type() == nil { - return NewDatabaseValueNull(), nil - } - - if !in.Type().Equal(t.TerraformType(ctx)) { - return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type()) - } - - if !in.IsKnown() { - return NewDatabaseValueUnknown(), nil - } - - if in.IsNull() { - return NewDatabaseValueNull(), nil - } - - attributes := map[string]attr.Value{} - - val := map[string]tftypes.Value{} - - err := in.As(&val) - - if err != nil { - return nil, err - } - - for k, v := range val { - a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v) - - if err != nil { - return nil, err - } - - attributes[k] = a - } - - return NewDatabaseValueMust(DatabaseValue{}.AttributeTypes(ctx), attributes), nil -} - -func (t DatabaseType) ValueType(ctx context.Context) attr.Value { - return DatabaseValue{} -} - -var _ basetypes.ObjectValuable = DatabaseValue{} - -type DatabaseValue struct { - Id basetypes.Int64Value `tfsdk:"id"` - Name basetypes.StringValue `tfsdk:"name"` - Owner basetypes.StringValue `tfsdk:"owner"` - state attr.ValueState -} - -func (v DatabaseValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - attrTypes := make(map[string]tftypes.Type, 3) - - var val tftypes.Value - var err error - - attrTypes["id"] = basetypes.Int64Type{}.TerraformType(ctx) - attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx) - attrTypes["owner"] = basetypes.StringType{}.TerraformType(ctx) - - objectType := tftypes.Object{AttributeTypes: attrTypes} - - switch v.state { - case attr.ValueStateKnown: - vals := make(map[string]tftypes.Value, 3) - - val, err = v.Id.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["id"] = val - - val, err = v.Name.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["name"] = val - - val, err = v.Owner.ToTerraformValue(ctx) - - if err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - vals["owner"] = val - - if err := tftypes.ValidateValue(objectType, vals); err != nil { - return tftypes.NewValue(objectType, tftypes.UnknownValue), err - } - - return tftypes.NewValue(objectType, vals), nil - case attr.ValueStateNull: - return tftypes.NewValue(objectType, nil), nil - case attr.ValueStateUnknown: - return tftypes.NewValue(objectType, tftypes.UnknownValue), nil - default: - panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state)) - } -} - -func (v DatabaseValue) IsNull() bool { - return v.state == attr.ValueStateNull -} - -func (v DatabaseValue) IsUnknown() bool { - return v.state == attr.ValueStateUnknown -} - -func (v DatabaseValue) String() string { - return "DatabaseValue" -} - -func (v DatabaseValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) { - var diags diag.Diagnostics - - attributeTypes := map[string]attr.Type{ - "id": basetypes.Int64Type{}, - "name": basetypes.StringType{}, - "owner": basetypes.StringType{}, - } - - if v.IsNull() { - return types.ObjectNull(attributeTypes), diags - } - - if v.IsUnknown() { - return types.ObjectUnknown(attributeTypes), diags - } - - objVal, diags := types.ObjectValue( - attributeTypes, - map[string]attr.Value{ - "id": v.Id, - "name": v.Name, - "owner": v.Owner, - }, - ) - - return objVal, diags -} - -func (v DatabaseValue) Equal(o attr.Value) bool { - other, ok := o.(DatabaseValue) - - if !ok { - return false - } - - if v.state != other.state { - return false - } - - if v.state != attr.ValueStateKnown { - return true - } - - if !v.Id.Equal(other.Id) { - return false - } - - if !v.Name.Equal(other.Name) { - return false - } - - if !v.Owner.Equal(other.Owner) { - return false - } - - return true -} - -func (v DatabaseValue) Type(ctx context.Context) attr.Type { - return DatabaseType{ - basetypes.ObjectType{ - AttrTypes: v.AttributeTypes(ctx), - }, - } -} - -func (v DatabaseValue) AttributeTypes(ctx context.Context) map[string]attr.Type { - return map[string]attr.Type{ - "id": basetypes.Int64Type{}, - "name": basetypes.StringType{}, - "owner": basetypes.StringType{}, - } -} diff --git a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go index cf4b987c..d5683a6c 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/datasources_gen/database_data_source_gen.go @@ -4,8 +4,8 @@ package postgresflexalpha import ( "context" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "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" @@ -31,16 +31,29 @@ func DatabaseDataSourceSchema(ctx context.Context) schema.Schema { }, "name": schema.StringAttribute{ Computed: true, - Optional: true, Description: "The name of the database.", MarkdownDescription: "The name of the database.", }, "owner": schema.StringAttribute{ - Optional: true, 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", + ), + }, + }, }, } } @@ -51,7 +64,6 @@ type DatabaseModel struct { InstanceId types.String `tfsdk:"instance_id"` Name types.String `tfsdk:"name"` Owner types.String `tfsdk:"owner"` - InstanceId types.String `tfsdk:"instance_id"` ProjectId types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` } diff --git a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go index 8dff24a0..6affc956 100644 --- a/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go +++ b/stackit/internal/services/postgresflexalpha/database/resources_gen/database_resource_gen.go @@ -7,9 +7,6 @@ import ( "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/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "strings" "github.com/hashicorp/terraform-plugin-framework/resource/schema" ) diff --git a/stackit/internal/services/postgresflexalpha/user/datasource.go b/stackit/internal/services/postgresflexalpha/user/datasource.go index 782c5d73..ac5c30bf 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasource.go +++ b/stackit/internal/services/postgresflexalpha/user/datasource.go @@ -178,23 +178,4 @@ func handleReadError( http.StatusForbidden: fmt.Sprintf("Forbidden access to project %q.", projectId), }, ) - model.UserId = types.Int64Value(userId) - model.Username = types.StringPointerValue(user.Name) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - var roles []attr.Value - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(string(role))) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Region = types.StringValue(region) - model.Status = types.StringPointerValue(user.Status) - return nil } diff --git a/stackit/internal/services/postgresflexalpha/user/mapper.go b/stackit/internal/services/postgresflexalpha/user/mapper.go index 519f46e1..3eb38d34 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper.go @@ -53,11 +53,8 @@ func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSour } model.Id = types.Int64Value(userId) - model.Host = types.StringValue(user.GetHost()) - model.Port = types.Int64Value(user.GetPort()) model.Region = types.StringValue(region) model.Status = types.StringValue(user.GetStatus()) - model.ConnectionString = types.StringValue(user.GetConnectionString()) return nil } @@ -141,10 +138,7 @@ func mapResourceFields(userResp *postgresflex.GetUserResponse, model *ResourceMo } model.Roles = types.List(rolesSet) } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) model.Status = types.StringPointerValue(user.Status) - model.ConnectionString = types.StringPointerValue(user.ConnectionString) return nil } diff --git a/stackit/internal/services/postgresflexalpha/user/mapper_test.go b/stackit/internal/services/postgresflexalpha/user/mapper_test.go index 3e02d25b..a48c62ee 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper_test.go @@ -27,17 +27,14 @@ func TestMapDataSourceFields(t *testing.T) { testRegion, DataSourceModel{ UserModel: data.UserModel{ - Id: types.Int64Value(1), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue(""), - Roles: types.List(types.SetNull(types.StringType)), - Host: types.StringValue(""), - Port: types.Int64Value(0), - Status: types.StringValue(""), - Region: types.StringValue(testRegion), - ConnectionString: types.StringValue(""), + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue(""), + Roles: types.List(types.SetNull(types.StringType)), + Status: types.StringValue(""), + Region: types.StringValue(testRegion), }, TerraformID: types.StringValue("pid,region,iid,1"), }, @@ -52,8 +49,6 @@ func TestMapDataSourceFields(t *testing.T) { "", }, Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), }, testRegion, DataSourceModel{ @@ -73,11 +68,8 @@ func TestMapDataSourceFields(t *testing.T) { }, ), ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - Status: types.StringValue(""), - ConnectionString: types.StringValue(""), + Region: types.StringValue(testRegion), + Status: types.StringValue(""), }, TerraformID: types.StringValue("pid,region,iid,1"), }, @@ -86,28 +78,22 @@ func TestMapDataSourceFields(t *testing.T) { { "null_fields_and_int_conversions", &postgresflex.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Roles: &[]postgresflex.UserRole{}, - Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - Status: utils.Ptr("status"), - ConnectionString: utils.Ptr("connection_string"), + Id: utils.Ptr(int64(1)), + Roles: &[]postgresflex.UserRole{}, + Name: nil, + Status: utils.Ptr("status"), }, testRegion, DataSourceModel{ UserModel: data.UserModel{ - Id: types.Int64Value(1), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue(""), - Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})), - Host: types.StringValue(""), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue(""), + Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})), + Region: types.StringValue(testRegion), + Status: types.StringValue("status"), }, TerraformID: types.StringValue("pid,region,iid,1"), }, @@ -186,8 +172,6 @@ func TestMapFieldsCreate(t *testing.T) { Name: types.StringNull(), Roles: types.List(types.SetNull(types.StringType)), Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), Region: types.StringValue(testRegion), Status: types.StringNull(), ConnectionString: types.StringNull(), @@ -199,10 +183,9 @@ func TestMapFieldsCreate(t *testing.T) { { "simple_values", &postgresflex.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: utils.Ptr("username"), - ConnectionString: utils.Ptr("connection_string"), - Status: utils.Ptr("status"), + Id: utils.Ptr(int64(1)), + Name: utils.Ptr("username"), + Status: utils.Ptr("status"), }, testRegion, ResourceModel{ @@ -213,8 +196,6 @@ func TestMapFieldsCreate(t *testing.T) { Name: types.StringValue("username"), Roles: types.List(types.SetNull(types.StringType)), Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), Region: types.StringValue(testRegion), Status: types.StringValue("status"), ConnectionString: types.StringValue("connection_string"), @@ -226,10 +207,9 @@ func TestMapFieldsCreate(t *testing.T) { { "null_fields_and_int_conversions", &postgresflex.GetUserResponse{ - Id: utils.Ptr(int64(1)), - Name: nil, - ConnectionString: nil, - Status: nil, + Id: utils.Ptr(int64(1)), + Name: nil, + Status: nil, }, testRegion, ResourceModel{ @@ -240,8 +220,6 @@ func TestMapFieldsCreate(t *testing.T) { Name: types.StringNull(), Roles: types.List(types.SetNull(types.StringType)), Password: types.StringNull(), - Host: types.StringNull(), - Port: types.Int64Null(), Region: types.StringValue(testRegion), Status: types.StringNull(), ConnectionString: types.StringNull(), @@ -323,8 +301,6 @@ func TestMapFields(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringNull(), Roles: types.List(types.SetNull(types.StringType)), - Host: types.StringNull(), - Port: types.Int64Null(), Region: types.StringValue(testRegion), Status: types.StringNull(), ConnectionString: types.StringNull(), @@ -343,8 +319,6 @@ func TestMapFields(t *testing.T) { "", }, Name: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), }, testRegion, ResourceModel{ @@ -363,8 +337,6 @@ func TestMapFields(t *testing.T) { }, ), ), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), Region: types.StringValue(testRegion), Status: types.StringNull(), ConnectionString: types.StringNull(), @@ -378,8 +350,6 @@ func TestMapFields(t *testing.T) { &postgresflex.GetUserResponse{ Id: utils.Ptr(int64(1)), Name: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), }, testRegion, ResourceModel{ @@ -390,8 +360,6 @@ func TestMapFields(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringNull(), Roles: types.List(types.SetNull(types.StringType)), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), Region: types.StringValue(testRegion), Status: types.StringNull(), ConnectionString: types.StringNull(), diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index e55c5dee..e459f130 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -558,7 +559,7 @@ func (r *userResource) ImportState( tflog.Info(ctx, "postgresflexalpha user state imported") } -func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region string) error { +func mapFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -575,14 +576,14 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri } else { return fmt.Errorf("user id not present") } - model.Id = utils.BuildInternalTerraformId( + model.TerraformID = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), ) model.UserId = types.Int64Value(userId) - model.Username = types.StringPointerValue(user.Name) + model.Name = types.StringPointerValue(user.Name) if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) + model.Roles = types.List(types.SetNull(types.StringType)) } else { var roles []attr.Value for _, role := range *user.Roles { @@ -592,7 +593,7 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } - model.Roles = rolesSet + model.Roles = types.List(rolesSet) } model.Region = types.StringValue(region) model.Status = types.StringPointerValue(user.Status) diff --git a/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_fix.go b/stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go similarity index 100% rename from stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_fix.go rename to stackit/internal/services/sqlserverflexalpha/database/datasources_gen/database_data_source_gen.go -- 2.49.1 From 184e133a2ae812d4b3d0b21c8d16c0d043fb67f3 Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Mon, 9 Feb 2026 21:28:49 +0100 Subject: [PATCH 28/31] feat: refactor data source models and update mapping functions for improved consistency --- .../postgresflexalpha/database/datasource.go | 16 +- .../postgresflexalpha/database/mapper.go | 10 +- .../postgresflexalpha/database/mapper_test.go | 30 +- .../postgresflexalpha/database/resource.go | 30 +- .../flavor/functions_test.go | 4 +- .../postgresflexalpha/flavors/datasource.go | 17 +- .../postgresflexalpha/instance/datasource.go | 27 +- .../postgresflexalpha/instance/functions.go | 62 ++-- .../postgresflexalpha/instance/resource.go | 149 ++++++++-- .../postgresflexalpha/user/datasource.go | 14 +- .../services/postgresflexalpha/user/mapper.go | 12 +- .../postgresflexalpha/user/mapper_test.go | 237 +++++++-------- .../postgresflexalpha/user/resource.go | 39 ++- .../sqlserverflexalpha/database/datasource.go | 31 +- .../sqlserverflexalpha/database/resource.go | 17 +- .../sqlserverflexalpha/flavor/datasource.go | 32 +- .../sqlserverflexalpha/flavors/datasource.go | 18 +- .../sqlserverflexalpha/instance/datasource.go | 198 +++---------- .../sqlserverflexalpha/instance/functions.go | 80 ++--- .../sqlserverflexalpha/instance/resource.go | 279 ++---------------- .../sqlserverflexalpha/user/datasource.go | 16 +- .../user/datasource_test.go | 16 +- .../sqlserverflexalpha/user/resource.go | 57 ++-- .../sqlserverflexalpha/user/resource_test.go | 92 +++--- .../sqlserverflexbeta/database/datasource.go | 18 +- .../sqlserverflexbeta/database/resource.go | 60 +++- .../sqlserverflexbeta/flavors/datasource.go | 57 ++-- .../sqlserverflexbeta/instance/datasource.go | 15 +- .../sqlserverflexbeta/instance/functions.go | 4 +- .../sqlserverflexbeta/instance/resource.go | 57 +++- .../sqlserverflexbeta/user/datasource.go | 29 +- .../sqlserverflexbeta/user/functions.go | 105 ++++--- .../sqlserverflexbeta/user/resource.go | 32 +- .../wait/sqlserverflexbeta/wait_test.go | 137 +++++---- 34 files changed, 980 insertions(+), 1017 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/datasource.go b/stackit/internal/services/postgresflexalpha/database/datasource.go index e836bcde..4a89be17 100644 --- a/stackit/internal/services/postgresflexalpha/database/datasource.go +++ b/stackit/internal/services/postgresflexalpha/database/datasource.go @@ -19,12 +19,6 @@ import ( "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" ) -// DataSourceModel maps the data source schema data. -type DataSourceModel struct { - postgresflexalpha2.DatabaseModel - TerraformID types.String `tfsdk:"id"` -} - // Ensure the implementation satisfies the expected interfaces. var ( _ datasource.DataSource = &databaseDataSource{} @@ -35,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 @@ -89,7 +89,7 @@ func (r *databaseDataSource) Read( req datasource.ReadRequest, resp *datasource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel + var model dataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -141,7 +141,7 @@ func (r *databaseDataSource) 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, + model *dataSourceModel, projectId, region, instanceId string, diags *diag.Diagnostics, ) (*postgresflexalpha.ListDatabase, error) { diff --git a/stackit/internal/services/postgresflexalpha/database/mapper.go b/stackit/internal/services/postgresflexalpha/database/mapper.go index 61707ade..5785f4b7 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper.go @@ -9,10 +9,10 @@ import ( "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. +// mapFields maps fields from a ListDatabase API response to a resourceModel for the data source. func mapFields( source *postgresflexalpha.ListDatabase, - model *DataSourceModel, + model *dataSourceModel, region string, ) error { if source == nil { @@ -51,8 +51,8 @@ func mapFields( return nil } -// mapResourceFields maps fields from a ListDatabase API response to a ResourceModel for the resource. -func mapResourceFields(source *postgresflexalpha.ListDatabase, model *ResourceModel) error { +// 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") } @@ -80,7 +80,7 @@ func mapResourceFields(source *postgresflexalpha.ListDatabase, model *ResourceMo } // toCreatePayload converts the resource model to an API create payload. -func toCreatePayload(model *ResourceModel) (*postgresflexalpha.CreateDatabaseRequestPayload, error) { +func toCreatePayload(model *resourceModel) (*postgresflexalpha.CreateDatabaseRequestPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } diff --git a/stackit/internal/services/postgresflexalpha/database/mapper_test.go b/stackit/internal/services/postgresflexalpha/database/mapper_test.go index 57024a5d..a2f18c12 100644 --- a/stackit/internal/services/postgresflexalpha/database/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/database/mapper_test.go @@ -13,11 +13,11 @@ import ( func TestMapFields(t *testing.T) { type given struct { source *postgresflexalpha.ListDatabase - model *DataSourceModel + model *dataSourceModel region string } type expected struct { - model *DataSourceModel + model *dataSourceModel err bool } @@ -34,11 +34,11 @@ func TestMapFields(t *testing.T) { Name: utils.Ptr("my-db"), Owner: utils.Ptr("\"my-owner\""), }, - model: &DataSourceModel{}, + model: &dataSourceModel{}, region: "eu01", }, expected: expected{ - model: &DataSourceModel{ + model: &dataSourceModel{ DatabaseModel: datasource.DatabaseModel{ Id: types.Int64Value(1), Name: types.StringValue("my-db"), @@ -59,7 +59,7 @@ func TestMapFields(t *testing.T) { Id: utils.Ptr(int64(1)), Name: utils.Ptr("my-db"), }, - model: &DataSourceModel{ + model: &dataSourceModel{ DatabaseModel: datasource.DatabaseModel{ Id: types.Int64Value(1), ProjectId: types.StringValue("my-project"), @@ -69,7 +69,7 @@ func TestMapFields(t *testing.T) { region: "eu01", }, expected: expected{ - model: &DataSourceModel{ + model: &dataSourceModel{ DatabaseModel: datasource.DatabaseModel{ Id: types.Int64Value(1), Name: types.StringValue("my-db"), @@ -86,7 +86,7 @@ func TestMapFields(t *testing.T) { name: "should fail on nil source", given: given{ source: nil, - model: &DataSourceModel{}, + model: &dataSourceModel{}, }, expected: expected{err: true}, }, @@ -94,7 +94,7 @@ func TestMapFields(t *testing.T) { name: "should fail on nil source ID", given: given{ source: &postgresflexalpha.ListDatabase{Id: nil}, - model: &DataSourceModel{}, + model: &dataSourceModel{}, }, expected: expected{err: true}, }, @@ -128,10 +128,10 @@ func TestMapFields(t *testing.T) { func TestMapResourceFields(t *testing.T) { type given struct { source *postgresflexalpha.ListDatabase - model *ResourceModel + model *resourceModel } type expected struct { - model *ResourceModel + model *resourceModel err bool } @@ -148,10 +148,10 @@ func TestMapResourceFields(t *testing.T) { Name: utils.Ptr("my-db"), Owner: utils.Ptr("\"my-owner\""), }, - model: &ResourceModel{}, + model: &resourceModel{}, }, expected: expected{ - model: &ResourceModel{ + model: &resourceModel{ Id: types.Int64Value(1), Name: types.StringValue("my-db"), Owner: types.StringValue("my-owner"), @@ -163,7 +163,7 @@ func TestMapResourceFields(t *testing.T) { name: "should fail on nil source", given: given{ source: nil, - model: &ResourceModel{}, + model: &resourceModel{}, }, expected: expected{err: true}, }, @@ -188,7 +188,7 @@ func TestMapResourceFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { type given struct { - model *ResourceModel + model *resourceModel } type expected struct { payload *postgresflexalpha.CreateDatabaseRequestPayload @@ -203,7 +203,7 @@ func TestToCreatePayload(t *testing.T) { { name: "should convert model to payload", given: given{ - model: &ResourceModel{ + model: &resourceModel{ Name: types.StringValue("my-db"), Owner: types.StringValue("my-owner"), }, diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 3392f39e..59bde925 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -41,8 +41,13 @@ var ( extractErrorMessage = "Extracting identity data: %v" ) -// ResourceModel describes the resource data model. -type ResourceModel = postgresflexalpha2.DatabaseModel +// 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 { @@ -52,11 +57,6 @@ type DatabaseResourceIdentityModel struct { DatabaseID types.Int64 `tfsdk:"database_id"` } -// NewDatabaseResource is a helper function to simplify the provider implementation. -func NewDatabaseResource() resource.Resource { - return &databaseResource{} -} - // databaseResource is the resource implementation. type databaseResource struct { client *postgresflexalpha.APIClient @@ -69,7 +69,7 @@ func (r *databaseResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel ResourceModel + var configModel resourceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -79,7 +79,7 @@ func (r *databaseResource) ModifyPlan( return } - var planModel ResourceModel + var planModel resourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -199,7 +199,7 @@ func (r *databaseResource) Create( req resource.CreateRequest, resp *resource.CreateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -309,7 +309,7 @@ func (r *databaseResource) Read( req resource.ReadRequest, resp *resource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -380,7 +380,7 @@ func (r *databaseResource) Update( req resource.UpdateRequest, resp *resource.UpdateResponse, ) { - var model ResourceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -418,7 +418,7 @@ func (r *databaseResource) Update( ctx = tflog.SetField(ctx, "database_id", databaseId) // Retrieve values from state - var stateModel ResourceModel + var stateModel resourceModel diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -483,7 +483,7 @@ func (r *databaseResource) Delete( req resource.DeleteRequest, resp *resource.DeleteResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -591,7 +591,7 @@ func (r *databaseResource) ImportState( // extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model. func (r *databaseResource) extractIdentityData( - model ResourceModel, + model resourceModel, identity DatabaseResourceIdentityModel, ) (projectId, region, instanceId string, databaseId int64, err error) { if !model.DatabaseId.IsNull() && !model.DatabaseId.IsUnknown() { diff --git a/stackit/internal/services/postgresflexalpha/flavor/functions_test.go b/stackit/internal/services/postgresflexalpha/flavor/functions_test.go index db8fa3bf..90590716 100644 --- a/stackit/internal/services/postgresflexalpha/flavor/functions_test.go +++ b/stackit/internal/services/postgresflexalpha/flavor/functions_test.go @@ -12,8 +12,8 @@ type mockRequest struct { executeFunc func() (*postgresflex.GetFlavorsResponse, error) } -func (m *mockRequest) Page(_ int64) postgresflex.ApiGetFlavorsRequestRequest { return m } -func (m *mockRequest) Size(_ int64) postgresflex.ApiGetFlavorsRequestRequest { return m } +func (m *mockRequest) Page(_ int32) postgresflex.ApiGetFlavorsRequestRequest { return m } +func (m *mockRequest) Size(_ int32) postgresflex.ApiGetFlavorsRequestRequest { return m } func (m *mockRequest) Sort(_ postgresflex.FlavorSort) postgresflex.ApiGetFlavorsRequestRequest { return m } diff --git a/stackit/internal/services/postgresflexalpha/flavors/datasource.go b/stackit/internal/services/postgresflexalpha/flavors/datasource.go index 26be805b..44483018 100644 --- a/stackit/internal/services/postgresflexalpha/flavors/datasource.go +++ b/stackit/internal/services/postgresflexalpha/flavors/datasource.go @@ -21,12 +21,19 @@ func NewFlavorsDataSource() datasource.DataSource { return &flavorsDataSource{} } +// dataSourceModel maps the data source schema data. +type dataSourceModel = postgresflexalphaGen.FlavorsModel + type flavorsDataSource struct { client *postgresflexalpha.APIClient providerData core.ProviderData } -func (d *flavorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *flavorsDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_flavors" } @@ -35,7 +42,11 @@ func (d *flavorsDataSource) Schema(ctx context.Context, _ datasource.SchemaReque } // Configure adds the provider configured client to the data source. -func (d *flavorsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *flavorsDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -51,7 +62,7 @@ func (d *flavorsDataSource) Configure(ctx context.Context, req datasource.Config } func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data postgresflexalphaGen.FlavorsModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) diff --git a/stackit/internal/services/postgresflexalpha/instance/datasource.go b/stackit/internal/services/postgresflexalpha/instance/datasource.go index de0c5c74..95f7904b 100644 --- a/stackit/internal/services/postgresflexalpha/instance/datasource.go +++ b/stackit/internal/services/postgresflexalpha/instance/datasource.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "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/conversion" postgresflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/instance/datasources_gen" @@ -26,6 +27,12 @@ func NewInstanceDataSource() datasource.DataSource { return &instanceDataSource{} } +// dataSourceModel maps the data source schema data. +type dataSourceModel struct { + postgresflexalpha2.InstanceModel + TerraformID types.String `tfsdk:"id"` +} + // instanceDataSource is the data source implementation. type instanceDataSource struct { client *postgresflexalpha.APIClient @@ -33,12 +40,20 @@ type instanceDataSource struct { } // Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (r *instanceDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_postgresflexalpha_instance" } // Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (r *instanceDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -59,8 +74,12 @@ func (r *instanceDataSource) Schema(ctx context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model postgresflexalpha2.InstanceModel +func (r *instanceDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform + var model dataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/postgresflexalpha/instance/functions.go b/stackit/internal/services/postgresflexalpha/instance/functions.go index dc29abe4..862f88ff 100644 --- a/stackit/internal/services/postgresflexalpha/instance/functions.go +++ b/stackit/internal/services/postgresflexalpha/instance/functions.go @@ -14,26 +14,32 @@ import ( "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" ) -func mapGetInstanceResponseToModel(ctx context.Context, m *postgresflexalpharesource.InstanceModel, resp *postgresflex.GetInstanceResponse) error { - tflog.Debug(ctx, ">>>> MSH DEBUG <<<<", map[string]interface{}{ - "id": m.Id.ValueString(), - "instance_id": m.InstanceId.ValueString(), - "backup_schedule": m.BackupSchedule.ValueString(), - "flavor_id": m.FlavorId.ValueString(), - "encryption.kek_key_id": m.Encryption.KekKeyId.ValueString(), - "encryption.kek_key_ring_id": m.Encryption.KekKeyRingId.ValueString(), - "encryption.kek_key_version": m.Encryption.KekKeyVersion.ValueString(), - "encryption.service_account": m.Encryption.ServiceAccount.ValueString(), - "is_deletable": m.IsDeletable.ValueBool(), - "name": m.Name.ValueString(), - "status": m.Status.ValueString(), - "retention_days": m.RetentionDays.ValueInt64(), - "replicas": m.Replicas.ValueInt64(), - "network.instance_address": m.Network.InstanceAddress.ValueString(), - "network.router_address": m.Network.RouterAddress.ValueString(), - "version": m.Version.ValueString(), - "network.acl": m.Network.Acl.String(), - }) +func mapGetInstanceResponseToModel( + ctx context.Context, + m *postgresflexalpharesource.InstanceModel, + resp *postgresflex.GetInstanceResponse, +) error { + tflog.Debug( + ctx, ">>>> MSH DEBUG <<<<", map[string]interface{}{ + "id": m.Id.ValueString(), + "instance_id": m.InstanceId.ValueString(), + "backup_schedule": m.BackupSchedule.ValueString(), + "flavor_id": m.FlavorId.ValueString(), + "encryption.kek_key_id": m.Encryption.KekKeyId.ValueString(), + "encryption.kek_key_ring_id": m.Encryption.KekKeyRingId.ValueString(), + "encryption.kek_key_version": m.Encryption.KekKeyVersion.ValueString(), + "encryption.service_account": m.Encryption.ServiceAccount.ValueString(), + "is_deletable": m.IsDeletable.ValueBool(), + "name": m.Name.ValueString(), + "status": m.Status.ValueString(), + "retention_days": m.RetentionDays.ValueInt64(), + "replicas": m.Replicas.ValueInt64(), + "network.instance_address": m.Network.InstanceAddress.ValueString(), + "network.router_address": m.Network.RouterAddress.ValueString(), + "version": m.Version.ValueString(), + "network.acl": m.Network.Acl.String(), + }, + ) m.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) m.Encryption = postgresflexalpharesource.NewEncryptionValueNull() @@ -61,7 +67,11 @@ func mapGetInstanceResponseToModel(ctx context.Context, m *postgresflexalphareso 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(), + m.InstanceId.ValueString(), + ) } m.InstanceId = types.StringPointerValue(resp.Id) @@ -121,7 +131,11 @@ func mapGetInstanceResponseToModel(ctx context.Context, m *postgresflexalphareso return nil } -func mapGetDataInstanceResponseToModel(ctx context.Context, m *postgresflexalphadatasource.InstanceModel, resp *postgresflex.GetInstanceResponse) error { +func mapGetDataInstanceResponseToModel( + ctx context.Context, + m *dataSourceModel, + resp *postgresflex.GetInstanceResponse, +) error { m.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) handleEncryption(m, resp) m.ConnectionInfo.Host = types.StringValue(resp.ConnectionInfo.GetHost()) @@ -155,7 +169,7 @@ func mapGetDataInstanceResponseToModel(ctx context.Context, m *postgresflexalpha return nil } -func handleNetwork(ctx context.Context, m *postgresflexalphadatasource.InstanceModel, resp *postgresflex.GetInstanceResponse) error { +func handleNetwork(ctx context.Context, m *dataSourceModel, resp *postgresflex.GetInstanceResponse) error { netAcl, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl()) if diags.HasError() { return fmt.Errorf("failed converting network acl from response") @@ -187,7 +201,7 @@ func handleNetwork(ctx context.Context, m *postgresflexalphadatasource.InstanceM return nil } -func handleEncryption(m *postgresflexalphadatasource.InstanceModel, resp *postgresflex.GetInstanceResponse) { +func handleEncryption(m *dataSourceModel, resp *postgresflex.GetInstanceResponse) { keyId := "" if keyIdVal, ok := resp.Encryption.GetKekKeyIdOk(); ok { keyId = keyIdVal diff --git a/stackit/internal/services/postgresflexalpha/instance/resource.go b/stackit/internal/services/postgresflexalpha/instance/resource.go index f061f8bf..d3680c47 100644 --- a/stackit/internal/services/postgresflexalpha/instance/resource.go +++ b/stackit/internal/services/postgresflexalpha/instance/resource.go @@ -23,8 +23,6 @@ import ( wait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/postgresflexalpha" ) -const packageName = "postgresflexalpha" - // Ensure the implementation satisfies the expected interfaces. var ( _ resource.Resource = &instanceResource{} @@ -40,11 +38,8 @@ func NewInstanceResource() resource.Resource { return &instanceResource{} } -// instanceResource is the resource implementation. -type instanceResource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} +// resourceModel describes the resource data model. +type resourceModel = postgresflexalpha.InstanceModel type InstanceResourceIdentityModel struct { ProjectID types.String `tfsdk:"project_id"` @@ -52,8 +47,18 @@ type InstanceResourceIdentityModel struct { InstanceID types.String `tfsdk:"instance_id"` } -func (r *instanceResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data postgresflexalpha.InstanceModel +// instanceResource is the resource implementation. +type instanceResource struct { + client *postgresflex.APIClient + providerData core.ProviderData +} + +func (r *instanceResource) ValidateConfig( + ctx context.Context, + req resource.ValidateConfigRequest, + resp *resource.ValidateConfigResponse, +) { + var data resourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { @@ -72,7 +77,11 @@ func (r *instanceResource) ValidateConfig(ctx context.Context, req resource.Vali // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) ModifyPlan( + ctx context.Context, + req resource.ModifyPlanRequest, + resp *resource.ModifyPlanResponse, +) { // nolint:gocritic // function signature required by Terraform var configModel postgresflexalpha.InstanceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { @@ -149,7 +158,11 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp.Schema = schema } -func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { +func (r *instanceResource) IdentitySchema( + _ context.Context, + _ resource.IdentitySchemaRequest, + resp *resource.IdentitySchemaResponse, +) { resp.IdentitySchema = identityschema.Schema{ Attributes: map[string]identityschema.Attribute{ "project_id": identityschema.StringAttribute{ @@ -171,7 +184,7 @@ func (r *instanceResource) Create( req resource.CreateRequest, resp *resource.CreateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model postgresflexalpha.InstanceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -201,7 +214,11 @@ func (r *instanceResource) Create( payload := modelToCreateInstancePayload(netAcl, model, replVal) // Create new instance - createResp, err := r.client.CreateInstanceRequest(ctx, projectId, region).CreateInstanceRequestPayload(payload).Execute() + createResp, err := r.client.CreateInstanceRequest( + ctx, + projectId, + region, + ).CreateInstanceRequestPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "error creating instance", fmt.Sprintf("Calling API: %v", err)) return @@ -227,13 +244,23 @@ func (r *instanceResource) Create( waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait handler error: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating instance", + fmt.Sprintf("Wait handler error: %v", err), + ) return } err = mapGetInstanceResponseToModel(ctx, &model, waitResp) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Error creating model: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error creating instance", + fmt.Sprintf("Error creating model: %v", err), + ) return } @@ -246,7 +273,11 @@ func (r *instanceResource) Create( tflog.Info(ctx, "Postgres Flex instance created") } -func modelToCreateInstancePayload(netAcl []string, model postgresflexalpha.InstanceModel, replVal int32) postgresflex.CreateInstanceRequestPayload { +func modelToCreateInstancePayload( + netAcl []string, + model postgresflexalpha.InstanceModel, + replVal int32, +) postgresflex.CreateInstanceRequestPayload { var enc *postgresflex.InstanceEncryption if !model.Encryption.IsNull() && !model.Encryption.IsUnknown() { enc = &postgresflex.InstanceEncryption{ @@ -279,10 +310,14 @@ func modelToCreateInstancePayload(netAcl []string, model postgresflexalpha.Insta } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform functionErrorSummary := "read instance failed" - var model postgresflexalpha.InstanceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -371,7 +406,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r err = mapGetInstanceResponseToModel(ctx, &model, instanceResp) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, functionErrorSummary, fmt.Sprintf("Processing API payload: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + functionErrorSummary, + fmt.Sprintf("Processing API payload: %v", err), + ) return } @@ -396,8 +436,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model postgresflexalpha.InstanceModel +func (r *instanceResource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { // nolint:gocritic // function signature required by Terraform + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -475,15 +519,31 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = core.LogResponse(ctx) - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) + waitResp, err := wait.PartialUpdateInstanceWaitHandler( + ctx, + r.client, + projectId, + region, + instanceId, + ).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error updating instance", + fmt.Sprintf("Instance update waiting: %v", err), + ) return } err = mapGetInstanceResponseToModel(ctx, &model, waitResp) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error updating instance", + fmt.Sprintf("Processing API payload: %v", err), + ) return } @@ -496,8 +556,12 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model postgresflexalpha.InstanceModel +func (r *instanceResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { // nolint:gocritic // function signature required by Terraform + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -538,16 +602,24 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,region,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *instanceResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { ctx = core.InitProviderContext(ctx) if req.ID != "" { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, + core.LogAndAddError( + ctx, &resp.Diagnostics, "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), + fmt.Sprintf( + "Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", + req.ID, + ), ) return } @@ -573,10 +645,23 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS identityData.Region.ValueString(), identityData.InstanceID.ValueString(), ), - )...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), identityData.ProjectID.ValueString())...) + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("project_id"), + identityData.ProjectID.ValueString(), + )..., + ) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), identityData.InstanceID.ValueString())...) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("instance_id"), + identityData.InstanceID.ValueString(), + )..., + ) tflog.Info(ctx, "Postgres Flex instance state imported") } diff --git a/stackit/internal/services/postgresflexalpha/user/datasource.go b/stackit/internal/services/postgresflexalpha/user/datasource.go index ac5c30bf..b0cf9d3b 100644 --- a/stackit/internal/services/postgresflexalpha/user/datasource.go +++ b/stackit/internal/services/postgresflexalpha/user/datasource.go @@ -25,17 +25,17 @@ var ( _ datasource.DataSource = &userDataSource{} ) -// DataSourceModel maps the data source schema data. -type DataSourceModel struct { - postgresflexalpha.UserModel - TerraformID types.String `tfsdk:"id"` -} - // NewUserDataSource is a helper function to simplify the provider implementation. func NewUserDataSource() datasource.DataSource { return &userDataSource{} } +// dataSourceModel maps the data source schema data. +type dataSourceModel struct { + postgresflexalpha.UserModel + TerraformID types.String `tfsdk:"id"` +} + // userDataSource is the data source implementation. type userDataSource struct { client *postgresflex.APIClient @@ -90,7 +90,7 @@ func (r *userDataSource) Read( req datasource.ReadRequest, resp *datasource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel + var model dataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/postgresflexalpha/user/mapper.go b/stackit/internal/services/postgresflexalpha/user/mapper.go index 3eb38d34..2445cb16 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper.go @@ -13,7 +13,7 @@ import ( ) // mapDataSourceFields maps API response to data source model, preserving existing ID. -func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSourceModel, region string) error { +func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *dataSourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -68,7 +68,7 @@ func toPayloadRoles(roles *[]string) *[]postgresflex.UserRole { } // toUpdatePayload creates an API update payload from the resource model. -func toUpdatePayload(model *ResourceModel, roles *[]string) ( +func toUpdatePayload(model *resourceModel, roles *[]string) ( *postgresflex.UpdateUserRequestPayload, error, ) { @@ -86,7 +86,7 @@ func toUpdatePayload(model *ResourceModel, roles *[]string) ( } // toCreatePayload creates an API create payload from the resource model. -func toCreatePayload(model *ResourceModel, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { +func toCreatePayload(model *resourceModel, roles *[]string) (*postgresflex.CreateUserRequestPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } @@ -101,7 +101,7 @@ func toCreatePayload(model *ResourceModel, roles *[]string) (*postgresflex.Creat } // mapResourceFields maps API response to the resource model, preserving existing ID. -func mapResourceFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, region string) error { +func mapResourceFields(userResp *postgresflex.GetUserResponse, model *resourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -118,9 +118,7 @@ func mapResourceFields(userResp *postgresflex.GetUserResponse, model *ResourceMo } else { return fmt.Errorf("user id not present") } - model.TerraformID = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), - ) + model.Id = types.Int64Value(userId) model.UserId = types.Int64Value(userId) model.Name = types.StringPointerValue(user.Name) diff --git a/stackit/internal/services/postgresflexalpha/user/mapper_test.go b/stackit/internal/services/postgresflexalpha/user/mapper_test.go index a48c62ee..6eeff9f0 100644 --- a/stackit/internal/services/postgresflexalpha/user/mapper_test.go +++ b/stackit/internal/services/postgresflexalpha/user/mapper_test.go @@ -9,7 +9,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/utils" postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" data "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/datasources_gen" - resource "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" ) func TestMapDataSourceFields(t *testing.T) { @@ -18,14 +17,14 @@ func TestMapDataSourceFields(t *testing.T) { description string input *postgresflex.GetUserResponse region string - expected DataSourceModel + expected dataSourceModel isValid bool }{ { "default_values", &postgresflex.GetUserResponse{}, testRegion, - DataSourceModel{ + dataSourceModel{ UserModel: data.UserModel{ Id: types.Int64Value(1), UserId: types.Int64Value(1), @@ -51,7 +50,7 @@ func TestMapDataSourceFields(t *testing.T) { Name: utils.Ptr("username"), }, testRegion, - DataSourceModel{ + dataSourceModel{ UserModel: data.UserModel{ Id: types.Int64Value(1), @@ -84,7 +83,7 @@ func TestMapDataSourceFields(t *testing.T) { Status: utils.Ptr("status"), }, testRegion, - DataSourceModel{ + dataSourceModel{ UserModel: data.UserModel{ Id: types.Int64Value(1), UserId: types.Int64Value(1), @@ -103,28 +102,28 @@ func TestMapDataSourceFields(t *testing.T) { "nil_response", nil, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, { "nil_response_2", &postgresflex.GetUserResponse{}, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, { "no_resource_id", &postgresflex.GetUserResponse{}, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &DataSourceModel{ + state := &dataSourceModel{ UserModel: data.UserModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, @@ -155,7 +154,7 @@ func TestMapFieldsCreate(t *testing.T) { description string input *postgresflex.GetUserResponse region string - expected ResourceModel + expected resourceModel isValid bool }{ { @@ -164,19 +163,16 @@ func TestMapFieldsCreate(t *testing.T) { Id: utils.Ptr(int64(1)), }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - Roles: types.List(types.SetNull(types.StringType)), - Password: types.StringNull(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + resourceModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), }, true, }, @@ -188,19 +184,16 @@ func TestMapFieldsCreate(t *testing.T) { Status: utils.Ptr("status"), }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("username"), - Roles: types.List(types.SetNull(types.StringType)), - Password: types.StringNull(), - Region: types.StringValue(testRegion), - Status: types.StringValue("status"), - ConnectionString: types.StringValue("connection_string"), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + resourceModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("username"), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Region: types.StringValue(testRegion), + Status: types.StringValue("status"), + ConnectionString: types.StringValue("connection_string"), }, true, }, @@ -212,19 +205,16 @@ func TestMapFieldsCreate(t *testing.T) { Status: nil, }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - Roles: types.List(types.SetNull(types.StringType)), - Password: types.StringNull(), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + resourceModel{ + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Password: types.StringNull(), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), }, true, }, @@ -232,32 +222,30 @@ func TestMapFieldsCreate(t *testing.T) { "nil_response", nil, testRegion, - ResourceModel{}, + resourceModel{}, false, }, { "nil_response_2", &postgresflex.GetUserResponse{}, testRegion, - ResourceModel{}, + resourceModel{}, false, }, { "no_resource_id", &postgresflex.GetUserResponse{}, testRegion, - ResourceModel{}, + resourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &ResourceModel{ - UserModel: resource.UserModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - }, + state := &resourceModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, } err := mapResourceFields(tt.input, state, tt.region) @@ -284,7 +272,7 @@ func TestMapFields(t *testing.T) { description string input *postgresflex.GetUserResponse region string - expected ResourceModel + expected resourceModel isValid bool }{ { @@ -293,19 +281,16 @@ func TestMapFields(t *testing.T) { Id: utils.Ptr(int64(1)), }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - Id: types.Int64Value(1), - UserId: types.Int64Value(int64(1)), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - Roles: types.List(types.SetNull(types.StringType)), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + resourceModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(int64(1)), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), }, true, }, @@ -321,27 +306,24 @@ func TestMapFields(t *testing.T) { Name: utils.Ptr("username"), }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - Id: types.Int64Value(1), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("username"), - Roles: types.List( - types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, - ), + resourceModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("username"), + Roles: types.List( + types.SetValueMust( + types.StringType, []attr.Value{ + types.StringValue("role_1"), + types.StringValue("role_2"), + types.StringValue(""), + }, ), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + ), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), }, true, }, @@ -352,19 +334,16 @@ func TestMapFields(t *testing.T) { Name: nil, }, testRegion, - ResourceModel{ - UserModel: resource.UserModel{ - Id: types.Int64Value(1), - UserId: types.Int64Value(1), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - Roles: types.List(types.SetNull(types.StringType)), - Region: types.StringValue(testRegion), - Status: types.StringNull(), - ConnectionString: types.StringNull(), - }, - TerraformID: types.StringValue("pid,region,iid,1"), + resourceModel{ + Id: types.Int64Value(1), + UserId: types.Int64Value(1), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Name: types.StringNull(), + Roles: types.List(types.SetNull(types.StringType)), + Region: types.StringValue(testRegion), + Status: types.StringNull(), + ConnectionString: types.StringNull(), }, true, }, @@ -372,32 +351,30 @@ func TestMapFields(t *testing.T) { "nil_response", nil, testRegion, - ResourceModel{}, + resourceModel{}, false, }, { "nil_response_2", &postgresflex.GetUserResponse{}, testRegion, - ResourceModel{}, + resourceModel{}, false, }, { "no_resource_id", &postgresflex.GetUserResponse{}, testRegion, - ResourceModel{}, + resourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &ResourceModel{ - UserModel: resource.UserModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - }, + state := &resourceModel{ + ProjectId: tt.expected.ProjectId, + InstanceId: tt.expected.InstanceId, } err := mapResourceFields(tt.input, state, tt.region) if !tt.isValid && err == nil { @@ -420,14 +397,14 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *ResourceModel + input *resourceModel inputRoles *[]string expected *postgresflex.CreateUserRequestPayload isValid bool }{ { "default_values", - &ResourceModel{}, + &resourceModel{}, &[]string{}, &postgresflex.CreateUserRequestPayload{ Name: nil, @@ -437,10 +414,8 @@ func TestToCreatePayload(t *testing.T) { }, { "simple_values", - &ResourceModel{ - UserModel: resource.UserModel{ - Name: types.StringValue("username"), - }, + &resourceModel{ + Name: types.StringValue("username"), }, &[]string{ "role_1", @@ -457,10 +432,8 @@ func TestToCreatePayload(t *testing.T) { }, { "null_fields_and_int_conversions", - &ResourceModel{ - UserModel: resource.UserModel{ - Name: types.StringNull(), - }, + &resourceModel{ + Name: types.StringNull(), }, &[]string{ "", @@ -482,7 +455,7 @@ func TestToCreatePayload(t *testing.T) { }, { "nil_roles", - &ResourceModel{}, + &resourceModel{}, nil, nil, false, @@ -512,14 +485,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { description string - input *ResourceModel + input *resourceModel inputRoles *[]string expected *postgresflex.UpdateUserRequestPayload isValid bool }{ { "default_values", - &ResourceModel{}, + &resourceModel{}, &[]string{}, &postgresflex.UpdateUserRequestPayload{ Roles: &[]postgresflex.UserRole{}, @@ -528,10 +501,8 @@ func TestToUpdatePayload(t *testing.T) { }, { "default_values", - &ResourceModel{ - UserModel: resource.UserModel{ - Name: types.StringValue("username"), - }, + &resourceModel{ + Name: types.StringValue("username"), }, &[]string{ "role_1", @@ -548,10 +519,8 @@ func TestToUpdatePayload(t *testing.T) { }, { "null_fields_and_int_conversions", - &ResourceModel{ - UserModel: resource.UserModel{ - Name: types.StringNull(), - }, + &resourceModel{ + Name: types.StringNull(), }, &[]string{ "", @@ -572,7 +541,7 @@ func TestToUpdatePayload(t *testing.T) { }, { "nil_roles", - &ResourceModel{}, + &resourceModel{}, nil, nil, false, diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index e459f130..c1a11495 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -41,12 +41,14 @@ var ( extractErrorMessage = "Extracting identity data: %v" ) -// ResourceModel represents the Terraform resource state for a PostgreSQL Flex user. -type ResourceModel struct { - postgresflexalpha.UserModel - TerraformID types.String `tfsdk:"id"` +// NewUserResource is a helper function to simplify the provider implementation. +func NewUserResource() resource.Resource { + return &userResource{} } +// 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"` @@ -55,11 +57,6 @@ type UserResourceIdentityModel struct { UserID types.Int64 `tfsdk:"database_id"` } -// NewUserResource is a helper function to simplify the provider implementation. -func NewUserResource() resource.Resource { - return &userResource{} -} - // userResource implements the resource handling for a PostgreSQL Flex user. type userResource struct { client *postgresflex.APIClient @@ -73,7 +70,7 @@ func (r *userResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel ResourceModel + var configModel resourceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -83,7 +80,7 @@ func (r *userResource) ModifyPlan( return } - var planModel ResourceModel + var planModel resourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -153,7 +150,7 @@ func (r *userResource) Create( req resource.CreateRequest, resp *resource.CreateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -252,7 +249,7 @@ func (r *userResource) Read( req resource.ReadRequest, resp *resource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -312,7 +309,7 @@ func (r *userResource) Update( req resource.UpdateRequest, resp *resource.UpdateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -342,7 +339,7 @@ func (r *userResource) Update( ctx = core.InitProviderContext(ctx) // Retrieve values from state - var stateModel ResourceModel + var stateModel resourceModel diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -414,7 +411,7 @@ func (r *userResource) Delete( req resource.DeleteRequest, resp *resource.DeleteResponse, ) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -559,7 +556,7 @@ func (r *userResource) ImportState( tflog.Info(ctx, "postgresflexalpha user state imported") } -func mapFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, region string) error { +func mapFields(userResp *postgresflex.GetUserResponse, model *resourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -576,9 +573,7 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, reg } else { return fmt.Errorf("user id not present") } - model.TerraformID = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), strconv.FormatInt(userId, 10), - ) + model.UserId = types.Int64Value(userId) model.Name = types.StringPointerValue(user.Name) @@ -602,7 +597,7 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *ResourceModel, reg // getUserResource refreshes the resource state by calling the API and mapping the response to the model. // Returns true if the resource state was successfully refreshed, false if the resource does not exist. -func (r *userResource) getUserResource(ctx context.Context, model *ResourceModel, arg *clientArg) (bool, error) { +func (r *userResource) getUserResource(ctx context.Context, model *resourceModel, arg *clientArg) (bool, error) { if arg.userId > math.MaxInt32 { return false, errors.New("error in type conversion: int value too large (userId)") @@ -638,7 +633,7 @@ type clientArg struct { // extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model. func (r *userResource) extractIdentityData( - model ResourceModel, + model resourceModel, identity UserResourceIdentityModel, ) (*clientArg, error) { diff --git a/stackit/internal/services/sqlserverflexalpha/database/datasource.go b/stackit/internal/services/sqlserverflexalpha/database/datasource.go index cd796159..3c201b5a 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/datasource.go +++ b/stackit/internal/services/sqlserverflexalpha/database/datasource.go @@ -4,6 +4,8 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" @@ -12,6 +14,12 @@ import ( sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils" ) +// dataSourceModel maps the data source schema data. +type dataSourceModel struct { + sqlserverflexalphaGen.DatabaseModel + TerraformID types.String `tfsdk:"id"` +} + var _ datasource.DataSource = (*databaseDataSource)(nil) func NewDatabaseDataSource() datasource.DataSource { @@ -23,16 +31,31 @@ type databaseDataSource struct { providerData core.ProviderData } -func (d *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *databaseDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_database" } func (d *databaseDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = sqlserverflexalphaGen.DatabaseDataSourceSchema(ctx) + s := sqlserverflexalphaGen.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 = s } // Configure adds the provider configured client to the data source. -func (d *databaseDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *databaseDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -48,7 +71,7 @@ func (d *databaseDataSource) Configure(ctx context.Context, req datasource.Confi } func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data sqlserverflexalphaGen.DatabaseModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) diff --git a/stackit/internal/services/sqlserverflexalpha/database/resource.go b/stackit/internal/services/sqlserverflexalpha/database/resource.go index 52866a9c..3a41c205 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/database/resource.go @@ -25,6 +25,9 @@ var ( _ resource.ResourceWithModifyPlan = &databaseResource{} ) +// resourceModel describes the resource data model. +type resourceModel = sqlserverflexalphaGen.DatabaseModel + func NewDatabaseResource() resource.Resource { return &databaseResource{} } @@ -59,7 +62,10 @@ func (r *databaseResource) Configure( utils.UserAgentConfigOption(r.providerData.Version), } if r.providerData.PostgresFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(r.providerData.PostgresFlexCustomEndpoint)) + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(r.providerData.PostgresFlexCustomEndpoint), + ) } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(r.providerData.GetRegion())) } @@ -67,7 +73,10 @@ func (r *databaseResource) Configure( if err != nil { resp.Diagnostics.AddError( "Error configuring API client", - fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err), + fmt.Sprintf( + "Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", + err, + ), ) return } @@ -97,7 +106,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques } func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data sqlserverflexalphaGen.DatabaseModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -115,7 +124,7 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r *databaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data sqlserverflexalphaGen.DatabaseModel + var data resourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) diff --git a/stackit/internal/services/sqlserverflexalpha/flavor/datasource.go b/stackit/internal/services/sqlserverflexalpha/flavor/datasource.go index 1deb2beb..a48e7572 100644 --- a/stackit/internal/services/sqlserverflexalpha/flavor/datasource.go +++ b/stackit/internal/services/sqlserverflexalpha/flavor/datasource.go @@ -26,6 +26,11 @@ var ( _ datasource.DataSourceWithConfigure = &flavorDataSource{} ) +// NewFlavorDataSource is a helper function to simplify the provider implementation. +func NewFlavorDataSource() datasource.DataSource { + return &flavorDataSource{} +} + type FlavorModel struct { ProjectId types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` @@ -41,11 +46,6 @@ type FlavorModel struct { StorageClasses types.List `tfsdk:"storage_classes"` } -// NewFlavorDataSource is a helper function to simplify the provider implementation. -func NewFlavorDataSource() datasource.DataSource { - return &flavorDataSource{} -} - // flavorDataSource is the data source implementation. type flavorDataSource struct { client *sqlserverflexalpha.APIClient @@ -53,12 +53,20 @@ type flavorDataSource struct { } // Metadata returns the data source type name. -func (r *flavorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (r *flavorDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_flavor" } // Configure adds the provider configured client to the data source. -func (r *flavorDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (r *flavorDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -212,11 +220,13 @@ func (r *flavorDataSource) Read(ctx context.Context, req datasource.ReadRequest, model.MinGb = types.Int64Value(*f.MinGB) if f.StorageClasses == nil { - model.StorageClasses = types.ListNull(sqlserverflexalphaGen.StorageClassesType{ - ObjectType: basetypes.ObjectType{ - AttrTypes: sqlserverflexalphaGen.StorageClassesValue{}.AttributeTypes(ctx), + model.StorageClasses = types.ListNull( + sqlserverflexalphaGen.StorageClassesType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: sqlserverflexalphaGen.StorageClassesValue{}.AttributeTypes(ctx), + }, }, - }) + ) } else { var scList []attr.Value for _, sc := range *f.StorageClasses { diff --git a/stackit/internal/services/sqlserverflexalpha/flavors/datasource.go b/stackit/internal/services/sqlserverflexalpha/flavors/datasource.go index 27609fc5..c1d4de36 100644 --- a/stackit/internal/services/sqlserverflexalpha/flavors/datasource.go +++ b/stackit/internal/services/sqlserverflexalpha/flavors/datasource.go @@ -13,8 +13,12 @@ import ( sqlserverflexalphaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/flavors/datasources_gen" ) +// dataSourceModel maps the data source schema data. +type dataSourceModel = sqlserverflexalphaGen.FlavorsModel + var _ datasource.DataSource = (*flavorsDataSource)(nil) +// TODO: Use NewFlavorsDataSource when datasource is implemented func NewFlavorsDataSource() datasource.DataSource { return &flavorsDataSource{} } @@ -24,7 +28,11 @@ type flavorsDataSource struct { providerData core.ProviderData } -func (d *flavorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *flavorsDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_flavors" } @@ -33,7 +41,11 @@ func (d *flavorsDataSource) Schema(ctx context.Context, _ datasource.SchemaReque } // Configure adds the provider configured client to the data source. -func (d *flavorsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *flavorsDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -49,7 +61,7 @@ func (d *flavorsDataSource) Configure(ctx context.Context, req datasource.Config } func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data sqlserverflexalphaGen.FlavorsModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) diff --git a/stackit/internal/services/sqlserverflexalpha/instance/datasource.go b/stackit/internal/services/sqlserverflexalpha/instance/datasource.go index 3f8f787e..0d58140c 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/datasource.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/datasource.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" sqlserverflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/instance/datasources_gen" sqlserverflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/instance/resources_gen" @@ -20,6 +22,12 @@ import ( "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" ) +// dataSourceModel maps the data source schema data. +type dataSourceModel struct { + sqlserverflexalpha2.InstanceModel + TerraformID types.String `tfsdk:"id"` +} + // Ensure the implementation satisfies the expected interfaces. var ( _ datasource.DataSource = &instanceDataSource{} @@ -37,12 +45,20 @@ type instanceDataSource struct { } // Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (r *instanceDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_instance" } // Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (r *instanceDataSource) Configure( + ctx context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { var ok bool r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { @@ -59,167 +75,22 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi // Schema defines the schema for the data source. func (r *instanceDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - //descriptions := map[string]string{ - // "main": "SQLServer Flex ALPHA instance 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`\".", - // "instance_id": "ID of the SQLServer Flex instance.", - // "project_id": "STACKIT project ID to which the instance is associated.", - // "name": "Instance name.", - // "access_scope": "The access scope of the instance. (e.g. SNA)", - // "acl": "The Access Control List (ACL) for the SQLServer Flex instance.", - // "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *")`, - // "region": "The resource region. If not defined, the provider region is used.", - // "encryption": "The encryption block.", - // "network": "The network block.", - // "keyring_id": "STACKIT KMS - KeyRing ID of the encryption key to use.", - // "key_id": "STACKIT KMS - Key ID of the encryption key to use.", - // "key_version": "STACKIT KMS - Key version to use in the encryption key.", - // "service:account": "STACKIT KMS - service account to use in the encryption key.", - // "instance_address": "The returned instance IP address of the SQLServer Flex instance.", - // "router_address": "The returned router IP address of the SQLServer Flex instance.", - //} + s := sqlserverflexalpha.InstanceDataSourceSchema(ctx) + s.Attributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`\\\".", + Computed: true, + } - resp.Schema = sqlserverflexalpha.InstanceDataSourceSchema(ctx) - - //resp.Schema = schema.Schema{ - // Description: descriptions["main"], - // Attributes: map[string]schema.Attribute{ - // "id": schema.StringAttribute{ - // Description: descriptions["id"], - // 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"], - // Computed: true, - // }, - // "backup_schedule": schema.StringAttribute{ - // Description: descriptions["backup_schedule"], - // Computed: true, - // }, - // "is_deletable": schema.BoolAttribute{ - // Description: descriptions["is_deletable"], - // Computed: true, - // }, - // "flavor": schema.SingleNestedAttribute{ - // Computed: true, - // Attributes: map[string]schema.Attribute{ - // "id": schema.StringAttribute{ - // Computed: true, - // }, - // "description": schema.StringAttribute{ - // Computed: true, - // }, - // "cpu": schema.Int64Attribute{ - // Computed: true, - // }, - // "ram": schema.Int64Attribute{ - // Computed: true, - // }, - // "node_type": schema.StringAttribute{ - // Computed: true, - // }, - // }, - // }, - // "replicas": schema.Int64Attribute{ - // Computed: true, - // }, - // "storage": schema.SingleNestedAttribute{ - // Computed: true, - // Attributes: map[string]schema.Attribute{ - // "class": schema.StringAttribute{ - // Computed: true, - // }, - // "size": schema.Int64Attribute{ - // Computed: true, - // }, - // }, - // }, - // "version": schema.StringAttribute{ - // Computed: true, - // }, - // "status": schema.StringAttribute{ - // Computed: true, - // }, - // "edition": schema.StringAttribute{ - // Computed: true, - // }, - // "retention_days": schema.Int64Attribute{ - // Computed: true, - // }, - // "region": schema.StringAttribute{ - // // the region cannot be found, so it has to be passed - // Optional: true, - // Description: descriptions["region"], - // }, - // "encryption": schema.SingleNestedAttribute{ - // Computed: true, - // Attributes: map[string]schema.Attribute{ - // "key_id": schema.StringAttribute{ - // Description: descriptions["key_id"], - // Computed: true, - // }, - // "key_version": schema.StringAttribute{ - // Description: descriptions["key_version"], - // Computed: true, - // }, - // "keyring_id": schema.StringAttribute{ - // Description: descriptions["keyring_id"], - // Computed: true, - // }, - // "service_account": schema.StringAttribute{ - // Description: descriptions["service_account"], - // Computed: true, - // }, - // }, - // Description: descriptions["encryption"], - // }, - // "network": schema.SingleNestedAttribute{ - // Computed: true, - // Attributes: map[string]schema.Attribute{ - // "access_scope": schema.StringAttribute{ - // Description: descriptions["access_scope"], - // Computed: true, - // }, - // "instance_address": schema.StringAttribute{ - // Description: descriptions["instance_address"], - // Computed: true, - // }, - // "router_address": schema.StringAttribute{ - // Description: descriptions["router_address"], - // Computed: true, - // }, - // "acl": schema.ListAttribute{ - // Description: descriptions["acl"], - // ElementType: types.StringType, - // Computed: true, - // }, - // }, - // Description: descriptions["network"], - // }, - // }, - //} + resp.Schema = s } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - //var model sqlserverflexalpha2.InstanceModel - var model sqlserverflexalpha2.InstanceModel +func (r *instanceDataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { // nolint:gocritic // function signature required by Terraform + var model dataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -279,10 +150,15 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // } //} - err = mapResponseToModel(ctx, instanceResp, &model, resp.Diagnostics) + err = mapFields(ctx, instanceResp, &model, resp.Diagnostics) //err = mapFields(ctx, instanceResp, &model, storage, encryption, network, region) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) + core.LogAndAddError( + ctx, + &resp.Diagnostics, + "Error reading instance", + fmt.Sprintf("Processing API payload: %v", err), + ) return } // Set refreshed state diff --git a/stackit/internal/services/sqlserverflexalpha/instance/functions.go b/stackit/internal/services/sqlserverflexalpha/instance/functions.go index 783d95e1..77effc6c 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/functions.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/functions.go @@ -14,26 +14,21 @@ import ( sqlserverflexResGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/instance/resources_gen" ) -func mapResponseToModel( +// instanceModel is a type constraint for models that can be mapped from a GetInstanceResponse. +type instanceModel interface { + *dataSourceModel | *resourceModel +} + +func mapFields[T instanceModel]( ctx context.Context, resp *sqlserverflex.GetInstanceResponse, - m *sqlserverflexResGen.InstanceModel, + m T, tfDiags diag.Diagnostics, ) error { - m.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) - m.Edition = types.StringValue(string(resp.GetEdition())) - m.Encryption = handleEncryption(m, resp) - m.FlavorId = types.StringValue(resp.GetFlavorId()) - m.Id = types.StringValue(resp.GetId()) - m.InstanceId = types.StringValue(resp.GetId()) - m.IsDeletable = types.BoolValue(resp.GetIsDeletable()) - m.Name = types.StringValue(resp.GetName()) netAcl, diags := types.ListValueFrom(ctx, types.StringType, resp.Network.GetAcl()) tfDiags.Append(diags...) if diags.HasError() { - return fmt.Errorf( - "error converting network acl response value", - ) + return fmt.Errorf("error converting network acl response value") } net, diags := sqlserverflexResGen.NewNetworkValue( sqlserverflexResGen.NetworkValue{}.AttributeTypes(ctx), @@ -46,22 +41,8 @@ func mapResponseToModel( ) tfDiags.Append(diags...) if diags.HasError() { - return fmt.Errorf( - "error converting network response value", - "access_scope", - types.StringValue(string(resp.Network.GetAccessScope())), - "acl", - netAcl, - "instance_address", - types.StringValue(resp.Network.GetInstanceAddress()), - "router_address", - types.StringValue(resp.Network.GetRouterAddress()), - ) + return fmt.Errorf("error converting network response value") } - m.Network = net - m.Replicas = types.Int64Value(int64(resp.GetReplicas())) - m.RetentionDays = types.Int64Value(resp.GetRetentionDays()) - m.Status = types.StringValue(string(resp.GetStatus())) stor, diags := sqlserverflexResGen.NewStorageValue( sqlserverflexResGen.StorageValue{}.AttributeTypes(ctx), @@ -74,14 +55,47 @@ func mapResponseToModel( if diags.HasError() { return fmt.Errorf("error converting storage response value") } - m.Storage = stor - m.Version = types.StringValue(string(resp.GetVersion())) + // The interface conversion is safe due to the type constraint. + model := any(m) + + if rm, ok := model.(*resourceModel); ok { + rm.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) + rm.Edition = types.StringValue(string(resp.GetEdition())) + rm.Encryption = handleEncryption(rm.Encryption, resp) + rm.FlavorId = types.StringValue(resp.GetFlavorId()) + rm.Id = types.StringValue(resp.GetId()) + rm.InstanceId = types.StringValue(resp.GetId()) + rm.IsDeletable = types.BoolValue(resp.GetIsDeletable()) + rm.Name = types.StringValue(resp.GetName()) + rm.Network = net + rm.Replicas = types.Int64Value(int64(resp.GetReplicas())) + rm.RetentionDays = types.Int64Value(resp.GetRetentionDays()) + rm.Status = types.StringValue(string(resp.GetStatus())) + rm.Storage = stor + rm.Version = types.StringValue(string(resp.GetVersion())) + } else if dm, ok := model.(*dataSourceModel); ok { + dm.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) + dm.Edition = types.StringValue(string(resp.GetEdition())) + dm.Encryption = handleEncryption(dm.Encryption, resp) + dm.FlavorId = types.StringValue(resp.GetFlavorId()) + dm.Id = types.StringValue(resp.GetId()) + dm.InstanceId = types.StringValue(resp.GetId()) + dm.IsDeletable = types.BoolValue(resp.GetIsDeletable()) + dm.Name = types.StringValue(resp.GetName()) + dm.Network = net + dm.Replicas = types.Int64Value(int64(resp.GetReplicas())) + dm.RetentionDays = types.Int64Value(resp.GetRetentionDays()) + dm.Status = types.StringValue(string(resp.GetStatus())) + dm.Storage = stor + dm.Version = types.StringValue(string(resp.GetVersion())) + } + return nil } func handleEncryption( - m *sqlserverflexResGen.InstanceModel, + encryptionValue sqlserverflexResGen.EncryptionValue, resp *sqlserverflex.GetInstanceResponse, ) sqlserverflexResGen.EncryptionValue { if !resp.HasEncryption() || @@ -91,10 +105,10 @@ func handleEncryption( resp.Encryption.KekKeyVersion == nil || resp.Encryption.ServiceAccount == nil { - if m.Encryption.IsNull() || m.Encryption.IsUnknown() { + if encryptionValue.IsNull() || encryptionValue.IsUnknown() { return sqlserverflexResGen.NewEncryptionValueNull() } - return m.Encryption + return encryptionValue } enc := sqlserverflexResGen.NewEncryptionValueNull() diff --git a/stackit/internal/services/sqlserverflexalpha/instance/resource.go b/stackit/internal/services/sqlserverflexalpha/instance/resource.go index 9257c8df..2e58a355 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/resource.go @@ -37,23 +37,26 @@ var ( _ resource.ResourceWithIdentity = &instanceResource{} ) +// NewInstanceResource is a helper function to simplify the provider implementation. +func NewInstanceResource() resource.Resource { + return &instanceResource{} +} + //nolint:unused // TODO: remove if not needed later var validNodeTypes []string = []string{ "Single", "Replica", } +// resourceModel describes the resource data model. +type resourceModel = sqlserverflexalpha2.InstanceModel + type InstanceResourceIdentityModel struct { ProjectID types.String `tfsdk:"project_id"` Region types.String `tfsdk:"region"` InstanceID types.String `tfsdk:"instance_id"` } -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - // instanceResource is the resource implementation. type instanceResource struct { client *sqlserverflexalpha.APIClient @@ -140,27 +143,6 @@ var modifiersFileByte []byte // Schema defines the schema for the resource. func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - //descriptions := map[string]string{ - // "main": "SQLServer Flex ALPHA instance 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`\".", - // "instance_id": "ID of the SQLServer Flex instance.", - // "project_id": "STACKIT project ID to which the instance is associated.", - // "name": "Instance name.", - // "access_scope": "The access scope of the instance. (SNA | PUBLIC)", - // "flavor_id": "The flavor ID of the instance.", - // "acl": "The Access Control List (ACL) for the SQLServer Flex instance.", - // "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *")`, - // "region": "The resource region. If not defined, the provider region is used.", - // "encryption": "The encryption block.", - // "replicas": "The number of replicas of the SQLServer Flex instance.", - // "network": "The network block.", - // "keyring_id": "STACKIT KMS - KeyRing ID of the encryption key to use.", - // "key_id": "STACKIT KMS - Key ID of the encryption key to use.", - // "key_version": "STACKIT KMS - Key version to use in the encryption key.", - // "service:account": "STACKIT KMS - service account to use in the encryption key.", - // "instance_address": "The returned instance IP address of the SQLServer Flex instance.", - // "router_address": "The returned router IP address of the SQLServer Flex instance.", - //} schema := sqlserverflexalpha2.InstanceResourceSchema(ctx) @@ -176,234 +158,13 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, return } resp.Schema = schema - - //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(), - // }, - // }, - // "instance_id": schema.StringAttribute{ - // Description: descriptions["instance_id"], - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // Validators: []validator.String{ - // validate.UUID(), - // validate.NoSeparator(), - // }, - // }, - // "project_id": schema.StringAttribute{ - // Description: descriptions["project_id"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // }, - // Validators: []validator.String{ - // validate.UUID(), - // validate.NoSeparator(), - // }, - // }, - // "name": schema.StringAttribute{ - // Description: descriptions["name"], - // Required: true, - // Validators: []validator.String{ - // stringvalidator.LengthAtLeast(1), - // stringvalidator.RegexMatches( - // regexp.MustCompile("^[a-z]([-a-z0-9]*[a-z0-9])?$"), - // "must start with a letter, must have lower case letters, numbers or hyphens, and no hyphen at the end", - // ), - // }, - // }, - // "backup_schedule": schema.StringAttribute{ - // Description: descriptions["backup_schedule"], - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // "is_deletable": schema.BoolAttribute{ - // Description: descriptions["is_deletable"], - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.Bool{ - // boolplanmodifier.UseStateForUnknown(), - // }, - // }, - // "flavor_id": schema.StringAttribute{ - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // stringplanmodifier.UseStateForUnknown(), - // }, - // Required: true, - // }, - // "replicas": schema.Int64Attribute{ - // Computed: true, - // PlanModifiers: []planmodifier.Int64{ - // int64planmodifier.UseStateForUnknown(), - // }, - // }, - // "storage": schema.SingleNestedAttribute{ - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.Object{ - // objectplanmodifier.UseStateForUnknown(), - // }, - // Attributes: map[string]schema.Attribute{ - // "class": schema.StringAttribute{ - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // "size": schema.Int64Attribute{ - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.Int64{ - // int64planmodifier.UseStateForUnknown(), - // }, - // }, - // }, - // }, - // "version": schema.StringAttribute{ - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // "edition": schema.StringAttribute{ - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // "retention_days": schema.Int64Attribute{ - // Optional: true, - // Computed: true, - // PlanModifiers: []planmodifier.Int64{ - // int64planmodifier.UseStateForUnknown(), - // }, - // }, - // "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(), - // }, - // }, - // "status": schema.StringAttribute{ - // Optional: true, - // // must be computed to allow for storing the override value from the provider - // Computed: true, - // Description: descriptions["status"], - // }, - // "encryption": schema.SingleNestedAttribute{ - // Optional: true, - // PlanModifiers: []planmodifier.Object{ - // objectplanmodifier.RequiresReplace(), - // objectplanmodifier.UseStateForUnknown(), - // }, - // Attributes: map[string]schema.Attribute{ - // "key_id": schema.StringAttribute{ - // Description: descriptions["key_id"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // }, - // Validators: []validator.String{ - // validate.NoSeparator(), - // }, - // }, - // "key_version": schema.StringAttribute{ - // Description: descriptions["key_version"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // }, - // Validators: []validator.String{ - // validate.NoSeparator(), - // }, - // }, - // "keyring_id": schema.StringAttribute{ - // Description: descriptions["keyring_id"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // }, - // Validators: []validator.String{ - // validate.NoSeparator(), - // }, - // }, - // "service_account": schema.StringAttribute{ - // Description: descriptions["service_account"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // }, - // Validators: []validator.String{ - // validate.NoSeparator(), - // }, - // }, - // }, - // Description: descriptions["encryption"], - // }, - // "network": schema.SingleNestedAttribute{ - // Required: true, - // Attributes: map[string]schema.Attribute{ - // "access_scope": schema.StringAttribute{ - // Description: descriptions["access_scope"], - // Required: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.RequiresReplace(), - // stringplanmodifier.UseStateForUnknown(), - // }, - // Validators: []validator.String{ - // validate.NoSeparator(), - // }, - // }, - // "acl": schema.ListAttribute{ - // Description: descriptions["acl"], - // ElementType: types.StringType, - // Required: true, - // PlanModifiers: []planmodifier.List{ - // listplanmodifier.UseStateForUnknown(), - // }, - // }, - // "instance_address": schema.StringAttribute{ - // Description: descriptions["instance_address"], - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // "router_address": schema.StringAttribute{ - // Description: descriptions["router_address"], - // Computed: true, - // PlanModifiers: []planmodifier.String{ - // stringplanmodifier.UseStateForUnknown(), - // }, - // }, - // }, - // Description: descriptions["network"], - // }, - // }, - //} } -func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { +func (r *instanceResource) IdentitySchema( + _ context.Context, + _ resource.IdentitySchemaRequest, + resp *resource.IdentitySchemaResponse, +) { resp.IdentitySchema = identityschema.Schema{ Attributes: map[string]identityschema.Attribute{ "project_id": identityschema.StringAttribute{ @@ -425,7 +186,7 @@ func (r *instanceResource) Create( req resource.CreateRequest, resp *resource.CreateResponse, ) { // nolint:gocritic // function signature required by Terraform - var model sqlserverflexalpha2.InstanceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -528,7 +289,7 @@ func (r *instanceResource) Create( // Map response body to schema // err = mapFields(ctx, waitResp, &model, storage, encryption, network, region) - err = mapResponseToModel(ctx, waitResp, &model, resp.Diagnostics) + err = mapFields(ctx, waitResp, &model, resp.Diagnostics) if err != nil { core.LogAndAddError( ctx, @@ -554,7 +315,7 @@ func (r *instanceResource) Read( req resource.ReadRequest, resp *resource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model sqlserverflexalpha2.InstanceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -592,7 +353,7 @@ func (r *instanceResource) Read( ctx = core.LogResponse(ctx) // Map response body to schema - err = mapResponseToModel(ctx, instanceResp, &model, resp.Diagnostics) + err = mapFields(ctx, instanceResp, &model, resp.Diagnostics) if err != nil { core.LogAndAddError( ctx, @@ -629,7 +390,7 @@ func (r *instanceResource) Update( resp *resource.UpdateResponse, ) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan - var model sqlserverflexalpha2.InstanceModel + var model resourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -683,7 +444,7 @@ func (r *instanceResource) Update( } // Map response body to schema - err = mapResponseToModel(ctx, waitResp, &model, resp.Diagnostics) + err = mapFields(ctx, waitResp, &model, resp.Diagnostics) // err = mapFields(ctx, waitResp, &model, storage, encryption, network, region) if err != nil { core.LogAndAddError( @@ -709,7 +470,7 @@ func (r *instanceResource) Delete( resp *resource.DeleteResponse, ) { // nolint:gocritic // function signature required by Terraform // Retrieve values from state - var model sqlserverflexalpha2.InstanceModel + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/sqlserverflexalpha/user/datasource.go b/stackit/internal/services/sqlserverflexalpha/user/datasource.go index 9b083db0..282a713c 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/datasource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/datasource.go @@ -28,7 +28,12 @@ var ( _ datasource.DataSource = &userDataSource{} ) -type DataSourceModel struct { +// NewUserDataSource is a helper function to simplify the provider implementation. +func NewUserDataSource() datasource.DataSource { + return &userDataSource{} +} + +type dataSourceModel struct { Id types.String `tfsdk:"id"` // needed by TF UserId types.Int64 `tfsdk:"user_id"` InstanceId types.String `tfsdk:"instance_id"` @@ -42,11 +47,6 @@ type DataSourceModel struct { DefaultDatabase types.String `tfsdk:"default_database"` } -// NewUserDataSource is a helper function to simplify the provider implementation. -func NewUserDataSource() datasource.DataSource { - return &userDataSource{} -} - // userDataSource is the data source implementation. type userDataSource struct { client *sqlserverflexalpha.APIClient @@ -164,7 +164,7 @@ func (r *userDataSource) Read( req datasource.ReadRequest, resp *datasource.ReadResponse, ) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel + var model dataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -226,7 +226,7 @@ func (r *userDataSource) Read( tflog.Info(ctx, "SQLServer Flex instance read") } -func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *DataSourceModel, region string) error { +func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *dataSourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } diff --git a/stackit/internal/services/sqlserverflexalpha/user/datasource_test.go b/stackit/internal/services/sqlserverflexalpha/user/datasource_test.go index b98c2e53..bd1fa093 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/datasource_test.go +++ b/stackit/internal/services/sqlserverflexalpha/user/datasource_test.go @@ -16,14 +16,14 @@ func TestMapDataSourceFields(t *testing.T) { description string input *sqlserverflexalpha.GetUserResponse region string - expected DataSourceModel + expected dataSourceModel isValid bool }{ { "default_values", &sqlserverflexalpha.GetUserResponse{}, testRegion, - DataSourceModel{ + dataSourceModel{ Id: types.StringValue("pid,region,iid,1"), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -54,7 +54,7 @@ func TestMapDataSourceFields(t *testing.T) { DefaultDatabase: utils.Ptr("default_db"), }, testRegion, - DataSourceModel{ + dataSourceModel{ Id: types.StringValue("pid,region,iid,1"), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -85,7 +85,7 @@ func TestMapDataSourceFields(t *testing.T) { Port: utils.Ptr(int64(2123456789)), }, testRegion, - DataSourceModel{ + dataSourceModel{ Id: types.StringValue("pid,region,iid,1"), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), @@ -102,28 +102,28 @@ func TestMapDataSourceFields(t *testing.T) { "nil_response", nil, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, { "nil_response_2", &sqlserverflexalpha.GetUserResponse{}, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, { "no_resource_id", &sqlserverflexalpha.GetUserResponse{}, testRegion, - DataSourceModel{}, + dataSourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &DataSourceModel{ + state := &dataSourceModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource.go b/stackit/internal/services/sqlserverflexalpha/user/resource.go index c5cea986..9a95ab10 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha" + sqlserverflexalphagen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/user/resources_gen" sqlserverflexalphaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils" sqlserverflexalphaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha" @@ -39,26 +39,14 @@ var ( _ resource.ResourceWithModifyPlan = &userResource{} ) -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.Int64 `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - DefaultDatabase types.String `tfsdk:"default_database"` -} - // NewUserResource is a helper function to simplify the provider implementation. func NewUserResource() resource.Resource { return &userResource{} } +// resourceModel describes the resource data model. +type resourceModel = sqlserverflexalphagen.UserModel + // userResource is the resource implementation. type userResource struct { client *sqlserverflexalpha.APIClient @@ -93,7 +81,7 @@ func (r *userResource) ModifyPlan( 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 @@ -103,7 +91,7 @@ func (r *userResource) ModifyPlan( return } - var planModel Model + var planModel resourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -229,7 +217,7 @@ func (r *userResource) 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() { @@ -313,7 +301,7 @@ func (r *userResource) 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() { @@ -387,7 +375,7 @@ func (r *userResource) Delete( resp *resource.DeleteResponse, ) { // nolint:gocritic // function signature required by Terraform // Retrieve values from plan - var model Model + var model resourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -455,7 +443,7 @@ func (r *userResource) ImportState( tflog.Info(ctx, "SQLServer Flex user state imported") } -func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *Model, region string) error { +func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *resourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -468,12 +456,6 @@ func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *Mod return fmt.Errorf("user id not present") } userId := *user.Id - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), - region, - model.InstanceId.ValueString(), - strconv.FormatInt(userId, 10), - ) model.UserId = types.Int64Value(userId) model.Username = types.StringPointerValue(user.Username) @@ -491,11 +473,11 @@ func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *Mod if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } - model.Roles = rolesSet + model.Roles = types.List(rolesSet) } if model.Roles.IsNull() || model.Roles.IsUnknown() { - model.Roles = types.SetNull(types.StringType) + model.Roles = types.List(types.SetNull(types.StringType)) } model.Host = types.StringPointerValue(user.Host) @@ -507,7 +489,7 @@ func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *Mod return nil } -func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *Model, region string) error { +func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *resourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") } @@ -524,12 +506,7 @@ func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *Model, regio } else { return fmt.Errorf("user id not present") } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), - region, - model.InstanceId.ValueString(), - strconv.FormatInt(userId, 10), - ) + model.UserId = types.Int64Value(userId) model.Username = types.StringPointerValue(user.Username) @@ -542,11 +519,11 @@ func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *Model, regio if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } - model.Roles = rolesSet + model.Roles = types.List(rolesSet) } if model.Roles.IsNull() || model.Roles.IsUnknown() { - model.Roles = types.SetNull(types.StringType) + model.Roles = types.List(types.SetNull(types.StringType)) } model.Host = types.StringPointerValue(user.Host) @@ -556,7 +533,7 @@ func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *Model, regio } func toCreatePayload( - model *Model, + model *resourceModel, roles []sqlserverflexalpha.UserRole, ) (*sqlserverflexalpha.CreateUserRequestPayload, error) { if model == nil { diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource_test.go b/stackit/internal/services/sqlserverflexalpha/user/resource_test.go index ad6bbf5a..e7ddddb1 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource_test.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource_test.go @@ -16,7 +16,7 @@ func TestMapFieldsCreate(t *testing.T) { description string input *sqlserverflexalpha.CreateUserResponse region string - expected Model + expected resourceModel isValid bool }{ { @@ -26,13 +26,13 @@ func TestMapFieldsCreate(t *testing.T) { Password: utils.Ptr(""), }, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), + resourceModel{ + Id: types.Int64Value(1), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringNull(), - Roles: types.SetNull(types.StringType), + Roles: types.List(types.SetNull(types.StringType)), Password: types.StringValue(""), Host: types.StringNull(), Port: types.Int64Null(), @@ -57,18 +57,20 @@ func TestMapFieldsCreate(t *testing.T) { DefaultDatabase: utils.Ptr("default_db"), }, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,2"), + resourceModel{ + Id: types.Int64Value(2), UserId: types.Int64Value(2), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, + Roles: types.List( + types.SetValueMust( + types.StringType, []attr.Value{ + types.StringValue("role_1"), + types.StringValue("role_2"), + types.StringValue(""), + }, + ), ), Password: types.StringValue("password"), Host: types.StringValue("host"), @@ -90,13 +92,13 @@ func TestMapFieldsCreate(t *testing.T) { Port: utils.Ptr(int64(2123456789)), }, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,3"), + resourceModel{ + Id: types.Int64Value(3), UserId: types.Int64Value(3), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), + Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})), Password: types.StringValue(""), Host: types.StringNull(), Port: types.Int64Value(2123456789), @@ -110,21 +112,21 @@ func TestMapFieldsCreate(t *testing.T) { "nil_response", nil, testRegion, - Model{}, + resourceModel{}, false, }, { "nil_response_2", &sqlserverflexalpha.CreateUserResponse{}, testRegion, - Model{}, + resourceModel{}, false, }, { "no_resource_id", &sqlserverflexalpha.CreateUserResponse{}, testRegion, - Model{}, + resourceModel{}, false, }, { @@ -133,14 +135,14 @@ func TestMapFieldsCreate(t *testing.T) { Id: utils.Ptr(int64(1)), }, testRegion, - Model{}, + resourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &Model{ + state := &resourceModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } @@ -168,20 +170,20 @@ func TestMapFields(t *testing.T) { description string input *sqlserverflexalpha.GetUserResponse region string - expected Model + expected resourceModel isValid bool }{ { "default_values", &sqlserverflexalpha.GetUserResponse{}, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), + resourceModel{ + Id: types.Int64Value(1), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringNull(), - Roles: types.SetNull(types.StringType), + Roles: types.List(types.SetNull(types.StringType)), Host: types.StringNull(), Port: types.Int64Null(), Region: types.StringValue(testRegion), @@ -201,18 +203,20 @@ func TestMapFields(t *testing.T) { Port: utils.Ptr(int64(1234)), }, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,2"), + resourceModel{ + Id: types.Int64Value(2), UserId: types.Int64Value(2), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringValue("username"), - Roles: types.SetValueMust( - types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }, + Roles: types.List( + types.SetValueMust( + types.StringType, []attr.Value{ + types.StringValue("role_1"), + types.StringValue("role_2"), + types.StringValue(""), + }, + ), ), Host: types.StringValue("host"), Port: types.Int64Value(1234), @@ -230,13 +234,13 @@ func TestMapFields(t *testing.T) { Port: utils.Ptr(int64(2123456789)), }, testRegion, - Model{ - Id: types.StringValue("pid,region,iid,1"), + resourceModel{ + Id: types.Int64Value(1), UserId: types.Int64Value(1), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), + Roles: types.List(types.SetValueMust(types.StringType, []attr.Value{})), Host: types.StringNull(), Port: types.Int64Value(2123456789), Region: types.StringValue(testRegion), @@ -247,28 +251,28 @@ func TestMapFields(t *testing.T) { "nil_response", nil, testRegion, - Model{}, + resourceModel{}, false, }, { "nil_response_2", &sqlserverflexalpha.GetUserResponse{}, testRegion, - Model{}, + resourceModel{}, false, }, { "no_resource_id", &sqlserverflexalpha.GetUserResponse{}, testRegion, - Model{}, + resourceModel{}, false, }, } for _, tt := range tests { t.Run( tt.description, func(t *testing.T) { - state := &Model{ + state := &resourceModel{ ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, @@ -294,14 +298,14 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { description string - input *Model + input *resourceModel inputRoles []sqlserverflexalpha.UserRole expected *sqlserverflexalpha.CreateUserRequestPayload isValid bool }{ { "default_values", - &Model{}, + &resourceModel{}, []sqlserverflexalpha.UserRole{}, &sqlserverflexalpha.CreateUserRequestPayload{ Roles: &[]sqlserverflexalpha.UserRole{}, @@ -311,7 +315,7 @@ func TestToCreatePayload(t *testing.T) { }, { "default_values", - &Model{ + &resourceModel{ Username: types.StringValue("username"), }, []sqlserverflexalpha.UserRole{ @@ -329,7 +333,7 @@ func TestToCreatePayload(t *testing.T) { }, { "null_fields_and_int_conversions", - &Model{ + &resourceModel{ Username: types.StringNull(), }, []sqlserverflexalpha.UserRole{ @@ -352,7 +356,7 @@ func TestToCreatePayload(t *testing.T) { }, { "nil_roles", - &Model{ + &resourceModel{ Username: types.StringValue("username"), }, []sqlserverflexalpha.UserRole{}, diff --git a/stackit/internal/services/sqlserverflexbeta/database/datasource.go b/stackit/internal/services/sqlserverflexbeta/database/datasource.go index bb6c3038..063fe6d9 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/datasource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/datasource.go @@ -26,17 +26,21 @@ func NewDatabaseDataSource() datasource.DataSource { return &databaseDataSource{} } +type dataSourceModel struct { + sqlserverflexbetaGen.DatabaseModel + TfId types.String `tfsdk:"id"` +} + type databaseDataSource struct { client *sqlserverflexbetaPkg.APIClient providerData core.ProviderData } -type dsModel struct { - sqlserverflexbetaGen.DatabaseModel - TfId types.String `tfsdk:"id"` -} - -func (d *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *databaseDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database" } @@ -92,7 +96,7 @@ func (d *databaseDataSource) Configure( } func (d *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data dsModel + var data dataSourceModel readErr := "Read DB error" // Read Terraform configuration data into the model diff --git a/stackit/internal/services/sqlserverflexbeta/database/resource.go b/stackit/internal/services/sqlserverflexbeta/database/resource.go index 5ae1d6c4..3e622e59 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/resource.go @@ -35,6 +35,9 @@ func NewDatabaseResource() resource.Resource { return &databaseResource{} } +// resourceModel describes the resource data model. +type resourceModel = sqlserverflexbetaResGen.DatabaseModel + type databaseResource struct { client *sqlserverflexbeta.APIClient providerData core.ProviderData @@ -47,7 +50,11 @@ type DatabaseResourceIdentityModel struct { DatabaseName types.String `tfsdk:"database_name"` } -func (r *databaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *databaseResource) Metadata( + ctx context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database" } @@ -55,7 +62,11 @@ func (r *databaseResource) Schema(ctx context.Context, req resource.SchemaReques resp.Schema = sqlserverflexbetaResGen.DatabaseResourceSchema(ctx) } -func (r *databaseResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { +func (r *databaseResource) IdentitySchema( + _ context.Context, + _ resource.IdentitySchemaRequest, + resp *resource.IdentitySchemaResponse, +) { resp.IdentitySchema = identityschema.Schema{ Attributes: map[string]identityschema.Attribute{ "project_id": identityschema.StringAttribute{ @@ -91,7 +102,10 @@ func (r *databaseResource) Configure( utils.UserAgentConfigOption(r.providerData.Version), } if r.providerData.SQLServerFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint)) + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint), + ) } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(r.providerData.GetRegion())) } @@ -111,7 +125,7 @@ func (r *databaseResource) Configure( } func (r *databaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data sqlserverflexbetaResGen.DatabaseModel + var data resourceModel createErr := "DB create error" // Read Terraform plan data into the model @@ -243,7 +257,7 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques } func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data sqlserverflexbetaResGen.DatabaseModel + var data resourceModel readErr := "[Database Read]" // Read Terraform prior state data into the model @@ -298,7 +312,7 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r *databaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data sqlserverflexbetaResGen.DatabaseModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -329,7 +343,7 @@ func (r *databaseResource) Update(ctx context.Context, req resource.UpdateReques } func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data sqlserverflexbetaResGen.DatabaseModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -422,9 +436,13 @@ func (r *databaseResource) ImportState( idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, + core.LogAndAddError( + ctx, &resp.Diagnostics, "Error importing database", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id],[database_name] Got: %q", req.ID), + fmt.Sprintf( + "Expected import identifier with format: [project_id],[region],[instance_id],[database_name] Got: %q", + req.ID, + ), ) return } @@ -455,10 +473,28 @@ func (r *databaseResource) ImportState( return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), identityData.ProjectID.ValueString())...) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("project_id"), + identityData.ProjectID.ValueString(), + )..., + ) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), identityData.InstanceID.ValueString())...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_name"), identityData.DatabaseName.ValueString())...) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("instance_id"), + identityData.InstanceID.ValueString(), + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("database_name"), + identityData.DatabaseName.ValueString(), + )..., + ) resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go b/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go index 9492387d..b401e4ff 100644 --- a/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go +++ b/stackit/internal/services/sqlserverflexbeta/flavors/datasource.go @@ -14,9 +14,9 @@ import ( "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" - sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" + sqlserverflexbetaPkg "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" - sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen" + sqlserverflexbetaGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/flavors/datasources_gen" ) var _ datasource.DataSource = (*flavorsDataSource)(nil) @@ -27,27 +27,31 @@ func NewFlavorsDataSource() datasource.DataSource { return &flavorsDataSource{} } -type dsModel struct { +type dataSourceModel struct { sqlserverflexbetaGen.FlavorsModel - TfId types.String `tfsdk:"id"` + TerraformId types.String `tfsdk:"id"` } -type flavorsDataSource struct{ +type flavorsDataSource struct { client *sqlserverflexbetaPkg.APIClient providerData core.ProviderData } -func (d *flavorsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *flavorsDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_flavors" } func (d *flavorsDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = sqlserverflexbetaGen.FlavorsDataSourceSchema(ctx) - resp.Schema.Attributes["id"] = schema.StringAttribute{ - Computed: true, - Description: "The terraform internal identifier.", - MarkdownDescription: "The terraform internal identifier.", - } + resp.Schema.Attributes["id"] = schema.StringAttribute{ + Computed: true, + Description: "The terraform internal identifier.", + MarkdownDescription: "The terraform internal identifier.", + } } // Configure adds the provider configured client to the data source. @@ -66,10 +70,10 @@ func (d *flavorsDataSource) Configure( config.WithCustomAuth(d.providerData.RoundTripper), utils.UserAgentConfigOption(d.providerData.Version), } - if d.providerData.SqlserverflexbetaCustomEndpoint != "" { + if d.providerData.SQLServerFlexCustomEndpoint != "" { apiClientConfigOptions = append( apiClientConfigOptions, - config.WithEndpoint(d.providerData.SqlserverflexbetaCustomEndpoint), + config.WithEndpoint(d.providerData.SQLServerFlexCustomEndpoint), ) } else { apiClientConfigOptions = append( @@ -93,7 +97,7 @@ func (d *flavorsDataSource) Configure( } func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data dsModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -106,7 +110,8 @@ func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest projectId := data.ProjectId.ValueString() region := d.providerData.GetRegionWithOverride(data.Region) - flavorsId := data.FlavorsId.ValueString() + // TODO: implement right identifier for flavors + flavorsId := data.FlavorsModel.Flavors ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) @@ -114,8 +119,8 @@ func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest // TODO: implement needed fields ctx = tflog.SetField(ctx, "flavors_id", flavorsId) - // TODO: refactor to correct implementation - flavorsResp, err := d.client.GetFlavorsRequest(ctx, projectId, region, flavorsId).Execute() + // TODO: refactor to correct implementation + _, err := d.client.GetFlavorsRequest(ctx, projectId, region).Execute() if err != nil { utils.LogError( ctx, @@ -133,17 +138,17 @@ func (d *flavorsDataSource) Read(ctx context.Context, req datasource.ReadRequest ctx = core.LogResponse(ctx) - - data.TfId = utils.BuildInternalTerraformId(projectId, region, ..) + // TODO: refactor to correct implementation of internal tf id + data.TerraformId = utils.BuildInternalTerraformId(projectId, region) // TODO: fill remaining fields - // data.Flavors = types.Sometype(apiResponse.GetFlavors()) - // data.Page = types.Sometype(apiResponse.GetPage()) - // data.Pagination = types.Sometype(apiResponse.GetPagination()) - // data.ProjectId = types.Sometype(apiResponse.GetProjectId()) - // data.Region = types.Sometype(apiResponse.GetRegion()) - // data.Size = types.Sometype(apiResponse.GetSize()) - // data.Sort = types.Sometype(apiResponse.GetSort())// Save data into Terraform state + // data.Flavors = types.Sometype(apiResponse.GetFlavors()) + // data.Page = types.Sometype(apiResponse.GetPage()) + // data.Pagination = types.Sometype(apiResponse.GetPagination()) + // data.ProjectId = types.Sometype(apiResponse.GetProjectId()) + // data.Region = types.Sometype(apiResponse.GetRegion()) + // data.Size = types.Sometype(apiResponse.GetSize()) + // data.Sort = types.Sometype(apiResponse.GetSort())// Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) tflog.Info(ctx, fmt.Sprintf("%s read successful", errorPrefix)) diff --git a/stackit/internal/services/sqlserverflexbeta/instance/datasource.go b/stackit/internal/services/sqlserverflexbeta/instance/datasource.go index 842a4cfd..85834b26 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/datasource.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/datasource.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" @@ -25,12 +26,22 @@ func NewInstanceDataSource() datasource.DataSource { return &instanceDataSource{} } +// dataSourceModel maps the data source schema data. +type dataSourceModel struct { + sqlserverflexbetaGen.InstanceModel + TerraformID types.String `tfsdk:"id"` +} + type instanceDataSource struct { client *sqlserverflexbetaPkg.APIClient providerData core.ProviderData } -func (d *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *instanceDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_instance" } @@ -81,7 +92,7 @@ func (d *instanceDataSource) Configure( } func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data sqlserverflexbetaGen.InstanceModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) diff --git a/stackit/internal/services/sqlserverflexbeta/instance/functions.go b/stackit/internal/services/sqlserverflexbeta/instance/functions.go index e9e1db57..25f3af0c 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/functions.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/functions.go @@ -84,7 +84,7 @@ func mapResponseToModel( func mapDataResponseToModel( ctx context.Context, resp *sqlserverflexbeta.GetInstanceResponse, - m *sqlserverflexbetaDataGen.InstanceModel, + m *dataSourceModel, tfDiags diag.Diagnostics, ) error { m.BackupSchedule = types.StringValue(resp.GetBackupSchedule()) @@ -181,7 +181,7 @@ func handleEncryption( } func handleDSEncryption( - m *sqlserverflexbetaDataGen.InstanceModel, + m *dataSourceModel, resp *sqlserverflexbeta.GetInstanceResponse, ) sqlserverflexbetaDataGen.EncryptionValue { if !resp.HasEncryption() || diff --git a/stackit/internal/services/sqlserverflexbeta/instance/resource.go b/stackit/internal/services/sqlserverflexbeta/instance/resource.go index 5d0b47d8..0e9c0012 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/resource.go @@ -43,13 +43,20 @@ type instanceResource struct { providerData core.ProviderData } +// 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(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *instanceResource) Metadata( + ctx context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_instance" } @@ -57,7 +64,11 @@ func (r *instanceResource) Schema(ctx context.Context, req resource.SchemaReques resp.Schema = sqlserverflexbetaResGen.InstanceResourceSchema(ctx) } -func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { +func (r *instanceResource) IdentitySchema( + _ context.Context, + _ resource.IdentitySchemaRequest, + resp *resource.IdentitySchemaResponse, +) { resp.IdentitySchema = identityschema.Schema{ Attributes: map[string]identityschema.Attribute{ "project_id": identityschema.StringAttribute{ @@ -90,7 +101,10 @@ func (r *instanceResource) Configure( utils.UserAgentConfigOption(r.providerData.Version), } if r.providerData.SQLServerFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint)) + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint), + ) } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(r.providerData.GetRegion())) } @@ -121,7 +135,7 @@ func (r *instanceResource) ModifyPlan( if req.Config.Raw.IsNull() { return } - var configModel sqlserverflexbetaResGen.InstanceModel + var configModel resourceModel resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) if resp.Diagnostics.HasError() { return @@ -151,7 +165,7 @@ func (r *instanceResource) ModifyPlan( var modifiersFileByte []byte func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data sqlserverflexbetaResGen.InstanceModel + var data resourceModel crateErr := "[SQL Server Flex BETA - Create] error" // Read Terraform plan data into the model @@ -257,7 +271,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data sqlserverflexbetaResGen.InstanceModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -324,7 +338,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data sqlserverflexbetaResGen.InstanceModel + var data resourceModel updateInstanceError := "Error updating instance" resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) @@ -411,7 +425,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data sqlserverflexbetaResGen.InstanceModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -484,9 +498,13 @@ func (r *instanceResource) ImportState( idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, + core.LogAndAddError( + ctx, &resp.Diagnostics, "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), + fmt.Sprintf( + "Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", + req.ID, + ), ) return } @@ -512,10 +530,23 @@ func (r *instanceResource) ImportState( identityData.Region.ValueString(), identityData.InstanceID.ValueString(), ), - )...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), identityData.ProjectID.ValueString())...) + )..., + ) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("project_id"), + identityData.ProjectID.ValueString(), + )..., + ) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), identityData.InstanceID.ValueString())...) + resp.Diagnostics.Append( + resp.State.SetAttribute( + ctx, + path.Root("instance_id"), + identityData.InstanceID.ValueString(), + )..., + ) tflog.Info(ctx, "Sqlserverflexbeta instance state imported") } diff --git a/stackit/internal/services/sqlserverflexbeta/user/datasource.go b/stackit/internal/services/sqlserverflexbeta/user/datasource.go index df1a8033..e6491a0f 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/datasource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/datasource.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" @@ -25,12 +26,30 @@ func NewUserDataSource() datasource.DataSource { return &userDataSource{} } +type dataSourceModel struct { + DefaultDatabase types.String `tfsdk:"default_database"` + Host types.String `tfsdk:"host"` + Id types.Int64 `tfsdk:"id"` + InstanceId types.String `tfsdk:"instance_id"` + Port types.Int64 `tfsdk:"port"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Roles types.List `tfsdk:"roles"` + Status types.String `tfsdk:"status"` + UserId types.Int64 `tfsdk:"user_id"` + Username types.String `tfsdk:"username"` +} + type userDataSource struct { client *sqlserverflexbetaPkg.APIClient providerData core.ProviderData } -func (d *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *userDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_user" } @@ -59,7 +78,7 @@ func (d *userDataSource) Configure( } func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data sqlserverflexbetaGen.UserModel + var data dataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -72,13 +91,15 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r projectId := data.ProjectId.ValueString() region := d.providerData.GetRegionWithOverride(data.Region) - userId := data.UserId.ValueString() + instanceId := data.InstanceId.ValueString() + userId := data.UserId.ValueInt64() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "user_id", userId) - userResp, err := d.client.GetUserRequest(ctx, projectId, region, userId).Execute() + userResp, err := d.client.GetUserRequest(ctx, projectId, region, instanceId, userId).Execute() if err != nil { utils.LogError( ctx, diff --git a/stackit/internal/services/sqlserverflexbeta/user/functions.go b/stackit/internal/services/sqlserverflexbeta/user/functions.go index b565f761..83ce641f 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/functions.go +++ b/stackit/internal/services/sqlserverflexbeta/user/functions.go @@ -3,13 +3,9 @@ package sqlserverflexbeta import ( "context" "fmt" - "math" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "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/conversion" sqlserverflexbeta "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" sqlserverflexbetaResGen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexbeta/instance/resources_gen" @@ -18,11 +14,39 @@ import ( func mapResponseToModel( ctx context.Context, resp *sqlserverflexbeta.GetUserResponse, - m *sqlserverflexbetaResGen.UserModel, + m *dataSourceModel, tfDiags diag.Diagnostics, ) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + m.Id = types.Int64Value(resp.GetId()) + m.UserId = types.Int64Value(resp.GetId()) + m.Username = types.StringValue(resp.GetUsername()) + m.Port = types.Int64Value(resp.GetPort()) + m.Host = types.StringValue(resp.GetHost()) + m.DefaultDatabase = types.StringValue(resp.GetDefaultDatabase()) + m.Status = types.StringValue(resp.GetStatus()) + + if resp.Roles != nil { + roles, diags := types.ListValueFrom(ctx, types.StringType, *resp.Roles) + tfDiags.Append(diags...) + if tfDiags.HasError() { + return fmt.Errorf("failed to map roles") + } + m.Roles = roles + } else { + m.Roles = types.ListNull(types.StringType) + } + + if resp.Status != nil { + m.Status = types.StringValue(*resp.Status) + } else { + m.Status = types.StringNull() + } + // TODO: complete and refactor - m.Id = types.StringValue(resp.GetId()) /* sampleList, diags := types.ListValueFrom(ctx, types.StringType, resp.GetList()) @@ -51,48 +75,63 @@ func mapResponseToModel( return nil } +// TODO: handle encryption field mapping when API supports it func handleEncryption( - m *sqlserverflexbetaResGen.UserModel, + m *dataSourceModel, resp *sqlserverflexbeta.GetUserResponse, ) sqlserverflexbetaResGen.EncryptionValue { - if !resp.HasEncryption() || - resp.Encryption == nil || - resp.Encryption.KekKeyId == nil || - resp.Encryption.KekKeyRingId == nil || - resp.Encryption.KekKeyVersion == nil || - resp.Encryption.ServiceAccount == nil { + /* + if !resp.HasEncryption() || - if m.Encryption.IsNull() || m.Encryption.IsUnknown() { - return sqlserverflexbetaResGen.NewEncryptionValueNull() + resp.Encryption == nil || + resp.Encryption.KekKeyId == nil || + resp.Encryption.KekKeyRingId == nil || + resp.Encryption.KekKeyVersion == nil || + resp.Encryption.ServiceAccount == nil { + + if m.Encryption.IsNull() || m.Encryption.IsUnknown() { + return sqlserverflexbetaResGen.NewEncryptionValueNull() + } + return m.Encryption } - return m.Encryption - } - enc := sqlserverflexbetaResGen.NewEncryptionValueNull() - if kVal, ok := resp.Encryption.GetKekKeyIdOk(); ok { - enc.KekKeyId = types.StringValue(kVal) - } - if kkVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok { - enc.KekKeyRingId = types.StringValue(kkVal) - } - if kkvVal, ok := resp.Encryption.GetKekKeyVersionOk(); ok { - enc.KekKeyVersion = types.StringValue(kkvVal) - } - if sa, ok := resp.Encryption.GetServiceAccountOk(); ok { - enc.ServiceAccount = types.StringValue(sa) - } - return enc + enc := sqlserverflexbetaResGen.NewEncryptionValueNull() + if kVal, ok := resp.Encryption.GetKekKeyIdOk(); ok { + enc.KekKeyId = types.StringValue(kVal) + } + if kkVal, ok := resp.Encryption.GetKekKeyRingIdOk(); ok { + enc.KekKeyRingId = types.StringValue(kkVal) + } + if kkvVal, ok := resp.Encryption.GetKekKeyVersionOk(); ok { + enc.KekKeyVersion = types.StringValue(kkvVal) + } + if sa, ok := resp.Encryption.GetServiceAccountOk(); ok { + enc.ServiceAccount = types.StringValue(sa) + } + return enc + */ + return sqlserverflexbetaResGen.NewEncryptionValueNull() } func toCreatePayload( ctx context.Context, - model *sqlserverflexbetaResGen.UserModel, + model *dataSourceModel, ) (*sqlserverflexbeta.CreateUserRequestPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } + var roles []sqlserverflexbeta.UserRole + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { + diags := model.Roles.ElementsAs(ctx, &roles, false) + if diags.HasError() { + return nil, fmt.Errorf("failed to convert roles: %v", diags) + } + } + return &sqlserverflexbeta.CreateUserRequestPayload{ - // TODO: fill fields + DefaultDatabase: model.DefaultDatabase.ValueStringPointer(), + Username: model.Username.ValueStringPointer(), + Roles: &roles, }, nil } diff --git a/stackit/internal/services/sqlserverflexbeta/user/resource.go b/stackit/internal/services/sqlserverflexbeta/user/resource.go index e2692e13..f8e1e747 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/resource.go @@ -33,6 +33,9 @@ func NewUserResource() resource.Resource { return &userResource{} } +// resourceModel describes the resource data model. +type resourceModel = sqlserverflexbetaResGen.UserModel + type userResource struct { client *sqlserverflexbeta.APIClient providerData core.ProviderData @@ -53,7 +56,11 @@ func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, r resp.Schema = sqlserverflexbetaResGen.UserResourceSchema(ctx) } -func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { +func (r *userResource) IdentitySchema( + _ context.Context, + _ resource.IdentitySchemaRequest, + resp *resource.IdentitySchemaResponse, +) { resp.IdentitySchema = identityschema.Schema{ Attributes: map[string]identityschema.Attribute{ "project_id": identityschema.StringAttribute{ @@ -85,8 +92,11 @@ func (r *userResource) Configure( config.WithCustomAuth(r.providerData.RoundTripper), utils.UserAgentConfigOption(r.providerData.Version), } - if r.providerData.SqlserverflexbetaCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(r.providerData.sqlserverflexbetaCustomEndpoint)) + if r.providerData.SQLServerFlexCustomEndpoint != "" { + apiClientConfigOptions = append( + apiClientConfigOptions, + config.WithEndpoint(r.providerData.SQLServerFlexCustomEndpoint), + ) } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(r.providerData.GetRegion())) } @@ -106,7 +116,7 @@ func (r *userResource) Configure( } func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data sqlserverflexbetaResGen.UserModel + var data resourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) @@ -159,14 +169,14 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r */ // Example data value setting - data.UserId = types.StringValue("id-from-response") + //data.UserId = types.StringValue("id-from-response") // TODO: Set data returned by API in identity identity := UserResourceIdentityModel{ ProjectID: types.StringValue(projectId), Region: types.StringValue(region), // TODO: add missing values - UserID: types.StringValue(UserId), + // UserID: types.StringValue(UserId), } resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) if resp.Diagnostics.HasError() { @@ -228,7 +238,7 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r } func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data sqlserverflexbetaResGen.UserModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -270,7 +280,7 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp } func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data sqlserverflexbetaResGen.UserModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -301,7 +311,7 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r } func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data sqlserverflexbetaResGen.UserModel + var data resourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -335,7 +345,7 @@ func (r *userResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel sqlserverflexbetaResGen.UserModel + var configModel resourceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -345,7 +355,7 @@ func (r *userResource) ModifyPlan( return } - var planModel sqlserverflexbetaResGen.UserModel + var planModel resourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return diff --git a/stackit/internal/wait/sqlserverflexbeta/wait_test.go b/stackit/internal/wait/sqlserverflexbeta/wait_test.go index e1ccc9c5..35b66cf6 100644 --- a/stackit/internal/wait/sqlserverflexbeta/wait_test.go +++ b/stackit/internal/wait/sqlserverflexbeta/wait_test.go @@ -20,7 +20,30 @@ type apiClientInstanceMocked struct { instanceGetFails bool } -func (a *apiClientInstanceMocked) GetInstanceRequestExecute(_ context.Context, _, _, _ string) (*sqlserverflex.GetInstanceResponse, error) { +func (a *apiClientInstanceMocked) GetDatabaseRequestExecute( + _ context.Context, + projectId string, + region string, + instanceId string, + databaseName string, +) (*sqlserverflex.GetDatabaseResponse, error) { + return nil, nil +} + +func (a *apiClientInstanceMocked) GetUserRequestExecute( + ctx context.Context, + projectId string, + region string, + instanceId string, + userId int64, +) (*sqlserverflex.GetUserResponse, error) { + return nil, nil +} + +func (a *apiClientInstanceMocked) GetInstanceRequestExecute( + _ context.Context, + _, _, _ string, +) (*sqlserverflex.GetInstanceResponse, error) { if a.instanceGetFails { return nil, &oapierror.GenericOpenAPIError{ StatusCode: 500, @@ -111,26 +134,28 @@ func TestCreateInstanceWaitHandler(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - instanceId := "foo-bar" + t.Run( + tt.desc, func(t *testing.T) { + instanceId := "foo-bar" - apiClient := &apiClientInstanceMocked{ - instanceId: instanceId, - instanceState: tt.instanceState, - instanceGetFails: tt.instanceGetFails, - } + apiClient := &apiClientInstanceMocked{ + instanceId: instanceId, + instanceState: tt.instanceState, + instanceGetFails: tt.instanceGetFails, + } - handler := CreateInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") + handler := CreateInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") - gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background()) - if (err != nil) != tt.wantErr { - t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) - } + gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background()) + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } - if !cmp.Equal(gotRes, tt.wantRes) { - t.Fatalf("handler gotRes = %v, want %v", gotRes, tt.wantRes) - } - }) + if !cmp.Equal(gotRes, tt.wantRes) { + t.Fatalf("handler gotRes = %v, want %v", gotRes, tt.wantRes) + } + }, + ) } } @@ -179,34 +204,36 @@ func TestUpdateInstanceWaitHandler(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - instanceId := "foo-bar" + t.Run( + tt.desc, func(t *testing.T) { + instanceId := "foo-bar" - apiClient := &apiClientInstanceMocked{ - instanceId: instanceId, - instanceState: tt.instanceState, - instanceGetFails: tt.instanceGetFails, - } - - var wantRes *sqlserverflex.GetInstanceResponse - if tt.wantResp { - wantRes = &sqlserverflex.GetInstanceResponse{ - Id: &instanceId, - Status: sqlserverflex.GetInstanceResponseGetStatusAttributeType(utils.Ptr(tt.instanceState)), + apiClient := &apiClientInstanceMocked{ + instanceId: instanceId, + instanceState: tt.instanceState, + instanceGetFails: tt.instanceGetFails, } - } - handler := UpdateInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") + var wantRes *sqlserverflex.GetInstanceResponse + if tt.wantResp { + wantRes = &sqlserverflex.GetInstanceResponse{ + Id: &instanceId, + Status: sqlserverflex.GetInstanceResponseGetStatusAttributeType(utils.Ptr(tt.instanceState)), + } + } - gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background()) + handler := UpdateInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") - if (err != nil) != tt.wantErr { - t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) - } - if !cmp.Equal(gotRes, wantRes) { - t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes) - } - }) + gotRes, err := handler.SetTimeout(10 * time.Millisecond).SetSleepBeforeWait(1 * time.Millisecond).WaitWithContext(context.Background()) + + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + if !cmp.Equal(gotRes, wantRes) { + t.Fatalf("handler gotRes = %v, want %v", gotRes, wantRes) + } + }, + ) } } @@ -236,23 +263,25 @@ func TestDeleteInstanceWaitHandler(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - instanceId := "foo-bar" + t.Run( + tt.desc, func(t *testing.T) { + instanceId := "foo-bar" - apiClient := &apiClientInstanceMocked{ - instanceGetFails: tt.instanceGetFails, - instanceIsDeleted: tt.instanceState == InstanceStateSuccess, - instanceId: instanceId, - instanceState: tt.instanceState, - } + apiClient := &apiClientInstanceMocked{ + instanceGetFails: tt.instanceGetFails, + instanceIsDeleted: tt.instanceState == InstanceStateSuccess, + instanceId: instanceId, + instanceState: tt.instanceState, + } - handler := DeleteInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") + handler := DeleteInstanceWaitHandler(context.Background(), apiClient, "", instanceId, "") - _, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) + _, err := handler.SetTimeout(10 * time.Millisecond).WaitWithContext(context.Background()) - if (err != nil) != tt.wantErr { - t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) - } - }) + if (err != nil) != tt.wantErr { + t.Fatalf("handler error = %v, wantErr %v", err, tt.wantErr) + } + }, + ) } } -- 2.49.1 From c4b31d0ba8661e1f7e5a8b71d944efd80d5ebcfb Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Tue, 10 Feb 2026 07:19:18 +0100 Subject: [PATCH 29/31] feat: refactor user and database resource import logic and identity handling --- .../postgresflexalpha/database/resource.go | 133 ++++++++--------- .../postgresflexalpha/instance/resource.go | 38 ++--- .../postgresflexalpha/user/resource.go | 138 ++++++++++-------- .../sqlserverflexalpha/database/resource.go | 115 ++++++++++++--- .../sqlserverflexalpha/instance/resource.go | 43 ++++-- .../sqlserverflexalpha/user/resource.go | 77 ++++++++-- .../sqlserverflexbeta/database/resource.go | 35 ++--- .../sqlserverflexbeta/instance/resource.go | 34 +---- .../sqlserverflexbeta/user/resource.go | 82 ++++++++--- 9 files changed, 415 insertions(+), 280 deletions(-) diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 59bde925..0914a0f0 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -13,7 +13,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/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" @@ -128,33 +127,6 @@ var modifiersFileByte []byte func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha2.DatabaseResourceSchema(ctx) - s.Attributes["project_id"] = schema.StringAttribute{ - Description: "STACKIT project ID to which the instance is associated.", - MarkdownDescription: "STACKIT project ID to which the instance is associated.", - Required: true, - } - s.Attributes["instance_id"] = schema.StringAttribute{ - Description: "ID of the PostgresFlex instance.", - MarkdownDescription: "ID of the PostgresFlex instance.", - Required: true, - } - s.Attributes["region"] = schema.StringAttribute{ - Description: "Region of the PostgresFlex instance.", - MarkdownDescription: "Region of the PostgresFlex instance.", - Optional: true, - Computed: true, - } - s.Attributes["database_id"] = schema.Int64Attribute{ - Description: "The ID of the database.", - Computed: true, - } - - s.Attributes["id"] = schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", - Optional: true, - Computed: true, - } - fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) if err != nil { resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) @@ -283,14 +255,14 @@ func (r *databaseResource) Create( return } - // Write identity attributes to state - identityData.ProjectID = types.StringValue(projectId) - identityData.Region = types.StringValue(region) - identityData.InstanceID = types.StringValue(instanceId) - identityData.DatabaseID = types.Int64Value(databaseId) - - resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) - + // 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 } @@ -537,56 +509,71 @@ func (r *databaseResource) ImportState( 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, - ), - ) - return - } - databaseId, err := strconv.ParseInt(idParts[3], 10, 64) - if err != nil { - core.LogAndAddError( + 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, - "Error importing database", - fmt.Sprintf("Invalid database_id format: %q. It must be a valid integer.", idParts[3]), + "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"), 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.", - ) - + // If no ID is provided, attempt to read identity attributes from the import configuration var identityData DatabaseResourceIdentityModel - identityData.ProjectID = types.StringValue(idParts[0]) - identityData.Region = types.StringValue(idParts[1]) - identityData.InstanceID = types.StringValue(idParts[2]) - identityData.DatabaseID = types.Int64Value(databaseId) - - resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) - + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "Postgres Flex instance state imported") + 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") } // extractIdentityData extracts essential identifiers from the resource model, falling back to the identity model. diff --git a/stackit/internal/services/postgresflexalpha/instance/resource.go b/stackit/internal/services/postgresflexalpha/instance/resource.go index d3680c47..ede60d42 100644 --- a/stackit/internal/services/postgresflexalpha/instance/resource.go +++ b/stackit/internal/services/postgresflexalpha/instance/resource.go @@ -82,7 +82,7 @@ func (r *instanceResource) ModifyPlan( req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse, ) { // nolint:gocritic // function signature required by Terraform - var configModel postgresflexalpha.InstanceModel + var configModel resourceModel // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return @@ -92,7 +92,7 @@ func (r *instanceResource) ModifyPlan( return } - var planModel postgresflexalpha.InstanceModel + var planModel resourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) if resp.Diagnostics.HasError() { return @@ -630,38 +630,20 @@ func (r *instanceResource) ImportState( 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 } - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("id"), - utils.BuildInternalTerraformId( - identityData.ProjectID.ValueString(), - identityData.Region.ValueString(), - identityData.InstanceID.ValueString(), - ), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("project_id"), - identityData.ProjectID.ValueString(), - )..., - ) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("instance_id"), - identityData.InstanceID.ValueString(), - )..., - ) + 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)...) tflog.Info(ctx, "Postgres Flex instance state imported") } diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index c1a11495..88e54171 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -219,6 +219,18 @@ func (r *userResource) Create( 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.Int64PointerValue(userResp.Id), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) + if resp.Diagnostics.HasError() { + return + } + // Verify creation exists, err := r.getUserResource(ctx, &model, arg) @@ -497,65 +509,6 @@ func (r *userResource) IdentitySchema( } } -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *userResource) ImportState( - 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 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 userId 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)...) - - core.LogAndAddWarning( - ctx, - &resp.Diagnostics, - "postgresflexalpha user imported with empty password and empty uri", - "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", - ) - - var identityData UserResourceIdentityModel - identityData.ProjectID = types.StringValue(idParts[0]) - identityData.Region = types.StringValue(idParts[1]) - identityData.InstanceID = types.StringValue(idParts[2]) - identityData.UserID = types.Int64Value(userId) - - resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) - - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Postgres Flex instance state imported") - tflog.Info(ctx, "postgresflexalpha user state imported") -} - func mapFields(userResp *postgresflex.GetUserResponse, model *resourceModel, region string) error { if userResp == nil { return fmt.Errorf("response is nil") @@ -631,6 +584,73 @@ type clientArg struct { userId int64 } +// ImportState imports a resource into the Terraform state on success. +// The expected import identifier format is: [project_id],[region],[instance_id],[database_id] +func (r *userResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + + 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") + + 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() { + 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)...) + 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, diff --git a/stackit/internal/services/sqlserverflexalpha/database/resource.go b/stackit/internal/services/sqlserverflexalpha/database/resource.go index 3a41c205..311eab29 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/database/resource.go @@ -3,10 +3,13 @@ package sqlserverflexalpha import ( "context" "fmt" + "strconv" "strings" "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" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha" @@ -23,13 +26,22 @@ var ( _ resource.ResourceWithConfigure = &databaseResource{} _ resource.ResourceWithImportState = &databaseResource{} _ resource.ResourceWithModifyPlan = &databaseResource{} + _ resource.ResourceWithIdentity = &databaseResource{} ) +func NewDatabaseResource() resource.Resource { + return &databaseResource{} +} + // resourceModel describes the resource data model. type resourceModel = sqlserverflexalphaGen.DatabaseModel -func NewDatabaseResource() resource.Resource { - return &databaseResource{} +// 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"` + DatabaseName types.String `tfsdk:"database_name"` } type databaseResource struct { @@ -45,6 +57,29 @@ func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp.Schema = sqlserverflexalphaGen.DatabaseResourceSchema(ctx) } +func (r *databaseResource) 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 + }, + "database_name": identityschema.StringAttribute{ + RequiredForImport: true, // can be defaulted by the provider configuration + }, + }, + } +} + // Configure adds the provider configured client to the resource. func (r *databaseResource) Configure( ctx context.Context, @@ -191,36 +226,72 @@ func (r *databaseResource) ModifyPlan( } // 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) - // Todo: Import logic - if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],..., 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_name], 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_name"), databaseId)...) + + core.LogAndAddWarning( + ctx, + &resp.Diagnostics, + "Sqlserverflexalpha 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, "Sqlserverflexalpha database state imported") + } + + // 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 } - 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])...) - // ... more ... + projectId := identityData.ProjectID.ValueString() + region := identityData.Region.ValueString() + instanceId := identityData.InstanceID.ValueString() + databaseName := identityData.DatabaseName.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("database_name"), databaseName)...) - core.LogAndAddWarning( - ctx, - &resp.Diagnostics, - "Sqlserverflexalpha 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, "Sqlserverflexalpha database state imported") } diff --git a/stackit/internal/services/sqlserverflexalpha/instance/resource.go b/stackit/internal/services/sqlserverflexalpha/instance/resource.go index 2e58a355..e9660107 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/resource.go @@ -515,20 +515,41 @@ func (r *instanceResource) ImportState( req resource.ImportStateRequest, resp *resource.ImportStateResponse, ) { - // TODO - idParts := strings.Split(req.ID, core.Separator) + if req.ID != "" { + 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), - ) + 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])...) 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 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)...) + tflog.Info(ctx, "SQLServer Flex instance state imported") } diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource.go b/stackit/internal/services/sqlserverflexalpha/user/resource.go index 9a95ab10..6d0fd495 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" @@ -47,6 +48,14 @@ func NewUserResource() resource.Resource { // resourceModel describes the resource data model. type resourceModel = sqlserverflexalphagen.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 is the resource implementation. type userResource struct { client *sqlserverflexalpha.APIClient @@ -417,23 +426,63 @@ func (r *userResource) ImportState( 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 user", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],[instance_id],[user_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 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") + 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"), idParts[3])...) + // 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() { + 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)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...) + core.LogAndAddWarning( ctx, &resp.Diagnostics, diff --git a/stackit/internal/services/sqlserverflexbeta/database/resource.go b/stackit/internal/services/sqlserverflexbeta/database/resource.go index 3e622e59..c4f6d718 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/resource.go @@ -467,39 +467,22 @@ func (r *databaseResource) ImportState( 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 } - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("project_id"), - identityData.ProjectID.ValueString(), - )..., - ) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("instance_id"), - identityData.InstanceID.ValueString(), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("database_name"), - identityData.DatabaseName.ValueString(), - )..., - ) + projectId := identityData.ProjectID.ValueString() + region := identityData.Region.ValueString() + instanceId := identityData.InstanceID.ValueString() + databaseName := identityData.DatabaseName.ValueString() - resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) - if resp.Diagnostics.HasError() { - return - } + 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_name"), databaseName)...) tflog.Info(ctx, "Sqlserverflexbeta database state imported") } diff --git a/stackit/internal/services/sqlserverflexbeta/instance/resource.go b/stackit/internal/services/sqlserverflexbeta/instance/resource.go index 0e9c0012..bdf07ed7 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/resource.go @@ -515,38 +515,20 @@ func (r *instanceResource) ImportState( 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 } - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("id"), - utils.BuildInternalTerraformId( - identityData.ProjectID.ValueString(), - identityData.Region.ValueString(), - identityData.InstanceID.ValueString(), - ), - )..., - ) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("project_id"), - identityData.ProjectID.ValueString(), - )..., - ) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), identityData.Region.ValueString())...) - resp.Diagnostics.Append( - resp.State.SetAttribute( - ctx, - path.Root("instance_id"), - identityData.InstanceID.ValueString(), - )..., - ) + 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)...) tflog.Info(ctx, "Sqlserverflexbeta instance state imported") } diff --git a/stackit/internal/services/sqlserverflexbeta/user/resource.go b/stackit/internal/services/sqlserverflexbeta/user/resource.go index f8e1e747..c196c75a 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/resource.go @@ -3,6 +3,7 @@ package sqlserverflexbeta import ( "context" "fmt" + "strconv" "strings" "github.com/hashicorp/terraform-plugin-framework/path" @@ -36,18 +37,19 @@ func NewUserResource() resource.Resource { // resourceModel describes the resource data model. type resourceModel = sqlserverflexbetaResGen.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:"database_id"` +} + type userResource struct { client *sqlserverflexbeta.APIClient providerData core.ProviderData } -type UserResourceIdentityModel struct { - ProjectID types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - UserID types.String `tfsdk:"instance_id"` - // TODO: implement further needed parts -} - func (r *userResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_user" } @@ -392,24 +394,61 @@ func (r *userResource) ImportState( req resource.ImportStateRequest, resp *resource.ImportStateResponse, ) { - idParts := strings.Split(req.ID, core.Separator) + 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, "Sqlserverflexbeta user state imported") - // Todo: Import logic - if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError( - ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf( - "Expected import identifier with format [project_id],[region],..., 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])...) - // ... more ... + // 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() { + 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)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), userId)...) core.LogAndAddWarning( ctx, @@ -418,4 +457,5 @@ func (r *userResource) ImportState( "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, "Sqlserverflexbeta user state imported") + } -- 2.49.1 From 062fd98870d82b3042545c14fae52a84c6120eae Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Tue, 10 Feb 2026 08:08:47 +0100 Subject: [PATCH 30/31] feat: add tests for plan modifiers configuration and resource schema handling --- .../utils/planModifiers.go | 0 stackit/internal/utils/planModifiers_test.go | 224 ++++++++++++++++++ 2 files changed, 224 insertions(+) rename stackit/internal/{services/postgresflexalpha => }/utils/planModifiers.go (100%) create mode 100644 stackit/internal/utils/planModifiers_test.go diff --git a/stackit/internal/services/postgresflexalpha/utils/planModifiers.go b/stackit/internal/utils/planModifiers.go similarity index 100% rename from stackit/internal/services/postgresflexalpha/utils/planModifiers.go rename to stackit/internal/utils/planModifiers.go diff --git a/stackit/internal/utils/planModifiers_test.go b/stackit/internal/utils/planModifiers_test.go new file mode 100644 index 00000000..337ea36f --- /dev/null +++ b/stackit/internal/utils/planModifiers_test.go @@ -0,0 +1,224 @@ +package utils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" +) + +func TestReadModifiersConfig(t *testing.T) { + testcases := []struct { + name string + content []byte + wantErr bool + }{ + { + name: "valid yaml", + content: []byte(` +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' +`), + wantErr: false, + }, + { + name: "invalid yaml", + content: []byte(`invalid: yaml: :`), + wantErr: true, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + _, err := ReadModifiersConfig(tc.content) + if (err != nil) != tc.wantErr { + t.Errorf("ReadModifiersConfig() error = %v, wantErr %v", err, tc.wantErr) + } + }, + ) + } +} + +func TestAddPlanModifiersToResourceSchema(t *testing.T) { + testcases := []struct { + name string + fields *Fields + sch *schema.Schema + wantErr bool + }{ + { + name: "full coverage - all types and nested structures", + fields: &Fields{ + Fields: []*Field{ + { + Name: "string_attr", + Modifiers: []*string{utils.Ptr("RequiresReplace"), utils.Ptr("UseStateForUnknown")}, + }, + {Name: "bool_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + {Name: "int_attr", Modifiers: []*string{utils.Ptr("UseStateForUnknown")}}, + {Name: "list_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + {Name: "Nested.sub_string", Modifiers: []*string{utils.Ptr("RequiresReplace")}}, + }, + }, + sch: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "StringAttr": schema.StringAttribute{}, + "BoolAttr": schema.BoolAttribute{}, + "IntAttr": schema.Int64Attribute{}, + "ListAttr": schema.ListAttribute{}, + "Nested": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "SubString": schema.StringAttribute{}, + }, + }, + "Unsupported": schema.MapAttribute{ElementType: types.StringType}, // Triggers default/warn case + }, + }, + wantErr: false, + }, + { + name: "validation error - invalid modifier", + fields: &Fields{ + Fields: []*Field{ + {Name: "id", Modifiers: []*string{utils.Ptr("InvalidModifier")}}, + }, + }, + sch: &schema.Schema{ + Attributes: map[string]schema.Attribute{"id": schema.StringAttribute{}}, + }, + wantErr: true, + }, + { + name: "validation error - empty modifier", + fields: &Fields{ + Fields: []*Field{ + {Name: "id", Modifiers: []*string{utils.Ptr("")}}, + }, + }, + sch: &schema.Schema{}, + wantErr: true, + }, + { + name: "nil fields - should return nil", + fields: nil, + sch: &schema.Schema{}, + wantErr: false, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + err := AddPlanModifiersToResourceSchema(tc.fields, tc.sch) + + if (err != nil) != tc.wantErr { + t.Fatalf("AddPlanModifiersToResourceSchema() error = %v, wantErr %v", err, tc.wantErr) + } + + if !tc.wantErr && tc.name == "full coverage - all types and nested structures" { + // Check StringAttr + if sAttr, ok := tc.sch.Attributes["StringAttr"].(schema.StringAttribute); ok { + if len(sAttr.PlanModifiers) != 2 { + t.Errorf("StringAttr: expected 2 modifiers, got %d", len(sAttr.PlanModifiers)) + } + } + + // Check Nested Sub-Attribute + if nested, ok := tc.sch.Attributes["Nested"].(schema.SingleNestedAttribute); ok { + if subAttr, ok := nested.Attributes["SubString"].(schema.StringAttribute); ok { + if len(subAttr.PlanModifiers) != 1 { + // Dies schlug vorher fehl, weil der Prefix "Nested" statt "nested" war + t.Errorf("Nested SubString: expected 1 modifier, got %d", len(subAttr.PlanModifiers)) + } + } else { + t.Error("SubString attribute not found in Nested") + } + } else { + t.Error("Nested attribute not found") + } + } + }, + ) + } +} + +func TestFieldListToMap(t *testing.T) { + testcases := []struct { + name string + fields *Fields + want map[string][]*string + }{ + { + name: "convert list to map", + fields: &Fields{ + Fields: []*Field{ + {Name: "test", Modifiers: []*string{utils.Ptr("mod")}}, + }, + }, + want: map[string][]*string{ + "test": {utils.Ptr("mod")}, + }, + }, + { + name: "nil fields", + fields: nil, + want: map[string][]*string{}, + }, + } + + for _, tc := range testcases { + t.Run( + tc.name, func(t *testing.T) { + got := fieldListToMap(tc.fields) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("fieldListToMap() mismatch (-want +got):\n%s", diff) + } + }, + ) + } +} + +func TestHandleTypeMismatches(t *testing.T) { + modifiers := []*string{utils.Ptr("RequiresReplace")} + + t.Run( + "bool type mismatch", func(t *testing.T) { + _, err := handleBoolPlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleBoolPlanModifiers") + } + }, + ) + + t.Run( + "string type mismatch", func(t *testing.T) { + _, err := handleStringPlanModifiers(schema.BoolAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleStringPlanModifiers") + } + }, + ) + + t.Run( + "int64 type mismatch", func(t *testing.T) { + _, err := handleInt64PlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleInt64PlanModifiers") + } + }, + ) + + t.Run( + "list type mismatch", func(t *testing.T) { + _, err := handleListPlanModifiers(schema.StringAttribute{}, modifiers) + if err == nil { + t.Error("expected error for type mismatch in handleListPlanModifiers") + } + }, + ) +} -- 2.49.1 From 9b573397762ff46690799aa0cd299a02eb9d1d0b Mon Sep 17 00:00:00 2001 From: Andre Harms Date: Tue, 10 Feb 2026 08:53:26 +0100 Subject: [PATCH 31/31] feat: refactor resource schema handling and add plan modifiers configuration --- .../postgresflexalpha/database/resource.go | 4 +- .../postgresflexalpha/instance/resource.go | 4 +- .../postgresflexalpha/user/resource.go | 10 +- .../database/planModifiers.yaml | 50 +++++++ .../sqlserverflexalpha/database/resource.go | 19 ++- .../sqlserverflexalpha/instance/resource.go | 5 +- .../user/planModifiers.yaml | 58 ++++++++ .../sqlserverflexalpha/user/resource.go | 129 +++--------------- .../database/planModifiers.yaml | 50 +++++++ .../sqlserverflexbeta/database/resource.go | 20 ++- .../sqlserverflexbeta/instance/resource.go | 21 ++- .../sqlserverflexbeta/user/planModifiers.yaml | 51 +++++++ .../sqlserverflexbeta/user/resource.go | 25 +++- 13 files changed, 312 insertions(+), 134 deletions(-) create mode 100644 stackit/internal/services/sqlserverflexalpha/database/planModifiers.yaml create mode 100644 stackit/internal/services/sqlserverflexalpha/user/planModifiers.yaml create mode 100644 stackit/internal/services/sqlserverflexbeta/database/planModifiers.yaml create mode 100644 stackit/internal/services/sqlserverflexbeta/user/planModifiers.yaml diff --git a/stackit/internal/services/postgresflexalpha/database/resource.go b/stackit/internal/services/postgresflexalpha/database/resource.go index 0914a0f0..64c62e70 100644 --- a/stackit/internal/services/postgresflexalpha/database/resource.go +++ b/stackit/internal/services/postgresflexalpha/database/resource.go @@ -127,13 +127,13 @@ var modifiersFileByte []byte func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha2.DatabaseResourceSchema(ctx) - fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + fields, err := utils.ReadModifiersConfig(modifiersFileByte) if err != nil { resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) return } - err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &s) + err = utils.AddPlanModifiersToResourceSchema(fields, &s) if err != nil { resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) return diff --git a/stackit/internal/services/postgresflexalpha/instance/resource.go b/stackit/internal/services/postgresflexalpha/instance/resource.go index ede60d42..78bb0572 100644 --- a/stackit/internal/services/postgresflexalpha/instance/resource.go +++ b/stackit/internal/services/postgresflexalpha/instance/resource.go @@ -144,13 +144,13 @@ var modifiersFileByte []byte // Schema defines the schema for the resource. func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { schema := postgresflexalpha.InstanceResourceSchema(ctx) - fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + fields, err := utils.ReadModifiersConfig(modifiersFileByte) if err != nil { resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) return } - err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &schema) + err = utils.AddPlanModifiersToResourceSchema(fields, &schema) if err != nil { resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) return diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 88e54171..b7698986 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" postgresflex "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/postgresflexalpha" postgresflexalpha "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/user/resources_gen" postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" @@ -124,19 +123,14 @@ var modifiersFileByte []byte // Schema defines the schema for the resource. func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { s := postgresflexalpha.UserResourceSchema(ctx) - s.Attributes["id"] = schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \\\"`project_id`,`region`,`instance_id`,`database_id`\\\".\",", - Optional: true, - Computed: true, - } - fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + fields, err := utils.ReadModifiersConfig(modifiersFileByte) if err != nil { resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) return } - err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &s) + err = utils.AddPlanModifiersToResourceSchema(fields, &s) if err != nil { resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) return diff --git a/stackit/internal/services/sqlserverflexalpha/database/planModifiers.yaml b/stackit/internal/services/sqlserverflexalpha/database/planModifiers.yaml new file mode 100644 index 00000000..d6209230 --- /dev/null +++ b/stackit/internal/services/sqlserverflexalpha/database/planModifiers.yaml @@ -0,0 +1,50 @@ +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' + + - name: 'instance_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'project_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'region' + modifiers: + - 'RequiresReplace' + + - name: 'name' + modifiers: + - 'RequiresReplace' + + - name: 'collation' + modifiers: + - 'RequiresReplace' + + - name: 'owner' + modifiers: + - 'RequiresReplace' + + - name: 'database_name' + modifiers: + - 'UseStateForUnknown' + + - name: 'collation_name' + modifiers: + - 'UseStateForUnknown' + + - name: 'compatibility' + modifiers: + - 'RequiresReplace' + + - name: 'compatibility_level' + modifiers: + - 'UseStateForUnknown' diff --git a/stackit/internal/services/sqlserverflexalpha/database/resource.go b/stackit/internal/services/sqlserverflexalpha/database/resource.go index 311eab29..f3dc6816 100644 --- a/stackit/internal/services/sqlserverflexalpha/database/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/database/resource.go @@ -2,6 +2,7 @@ package sqlserverflexalpha import ( "context" + _ "embed" "fmt" "strconv" "strings" @@ -53,8 +54,24 @@ func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequ resp.TypeName = req.ProviderTypeName + "_sqlserverflexalpha_database" } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + func (r *databaseResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = sqlserverflexalphaGen.DatabaseResourceSchema(ctx) + s := sqlserverflexalphaGen.DatabaseResourceSchema(ctx) + + fields, err := utils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return + } + + err = utils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } func (r *databaseResource) IdentitySchema( diff --git a/stackit/internal/services/sqlserverflexalpha/instance/resource.go b/stackit/internal/services/sqlserverflexalpha/instance/resource.go index e9660107..76c73639 100644 --- a/stackit/internal/services/sqlserverflexalpha/instance/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/instance/resource.go @@ -11,7 +11,6 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" - postgresflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils" sqlserverflexalpha2 "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/instance/resources_gen" sqlserverflexUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils" @@ -146,13 +145,13 @@ func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, schema := sqlserverflexalpha2.InstanceResourceSchema(ctx) - fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte) + fields, err := utils.ReadModifiersConfig(modifiersFileByte) if err != nil { resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) return } - err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &schema) + err = utils.AddPlanModifiersToResourceSchema(fields, &schema) if err != nil { resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) return diff --git a/stackit/internal/services/sqlserverflexalpha/user/planModifiers.yaml b/stackit/internal/services/sqlserverflexalpha/user/planModifiers.yaml new file mode 100644 index 00000000..b01aae98 --- /dev/null +++ b/stackit/internal/services/sqlserverflexalpha/user/planModifiers.yaml @@ -0,0 +1,58 @@ +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' + + - name: 'user_id' + modifiers: + - 'UseStateForUnknown' + + - name: 'instance_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'project_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'username' + modifiers: + - 'RequiresReplace' + + - name: 'roles' + modifiers: + - 'RequiresReplace' + + - name: 'password' + modifiers: + - 'UseStateForUnknown' + + - name: 'host' + modifiers: + - 'UseStateForUnknown' + + - name: 'port' + modifiers: + - 'UseStateForUnknown' + + - name: 'region' + modifiers: + - 'RequiresReplace' + + - name: 'status' + modifiers: + - 'UseStateForUnknown' + + - name: 'uri' + modifiers: + - 'UseStateForUnknown' + + - name: 'default_database' + modifiers: + - 'UseStateForUnknown' diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource.go b/stackit/internal/services/sqlserverflexalpha/user/resource.go index 6d0fd495..41e6e1c5 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource.go @@ -2,34 +2,27 @@ package sqlserverflexalpha import ( "context" + _ "embed" "errors" "fmt" "net/http" "strconv" "strings" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexalpha" sqlserverflexalphagen "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/user/resources_gen" sqlserverflexalphaUtils "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/services/sqlserverflexalpha/utils" sqlserverflexalphaWait "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/wait/sqlserverflexalpha" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "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" - "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/attr" "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/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "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/stackit/internal/conversion" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" ) // Ensure the implementation satisfies the expected interfaces. @@ -117,107 +110,25 @@ func (r *userResource) ModifyPlan( } } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + // Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SQLServer Flex user 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`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the SQLServer Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "Username of the SQLServer Flex instance.", - "roles": "Database access levels for the user. The values for the default roles are: `##STACKIT_DatabaseManager##`, `##STACKIT_LoginManager##`, `##STACKIT_ProcessManager##`, `##STACKIT_ServerManager##`, `##STACKIT_SQLAgentManager##`, `##STACKIT_SQLAgentUser##`", - "password": "Password of the user account.", - "status": "Status of the user.", - "default_database": "Default database of the user.", +func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + s := sqlserverflexalphagen.UserResourceSchema(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(), - }, - }, - "user_id": schema.Int64Attribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{}, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "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(), - }, - }, - "status": schema.StringAttribute{ - Computed: true, - }, - "default_database": schema.StringAttribute{ - Computed: true, - }, - }, + err = utils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return } + resp.Schema = s } // Create creates the resource and sets the initial Terraform state. diff --git a/stackit/internal/services/sqlserverflexbeta/database/planModifiers.yaml b/stackit/internal/services/sqlserverflexbeta/database/planModifiers.yaml new file mode 100644 index 00000000..d6209230 --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/database/planModifiers.yaml @@ -0,0 +1,50 @@ +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' + + - name: 'instance_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'project_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'RequiresReplace' + + - name: 'region' + modifiers: + - 'RequiresReplace' + + - name: 'name' + modifiers: + - 'RequiresReplace' + + - name: 'collation' + modifiers: + - 'RequiresReplace' + + - name: 'owner' + modifiers: + - 'RequiresReplace' + + - name: 'database_name' + modifiers: + - 'UseStateForUnknown' + + - name: 'collation_name' + modifiers: + - 'UseStateForUnknown' + + - name: 'compatibility' + modifiers: + - 'RequiresReplace' + + - name: 'compatibility_level' + modifiers: + - 'UseStateForUnknown' diff --git a/stackit/internal/services/sqlserverflexbeta/database/resource.go b/stackit/internal/services/sqlserverflexbeta/database/resource.go index c4f6d718..9c052dbb 100644 --- a/stackit/internal/services/sqlserverflexbeta/database/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/database/resource.go @@ -2,6 +2,7 @@ package sqlserverflexbeta import ( "context" + _ "embed" "fmt" "strings" "time" @@ -58,8 +59,25 @@ func (r *databaseResource) Metadata( resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_database" } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + func (r *databaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = sqlserverflexbetaResGen.DatabaseResourceSchema(ctx) + + s := sqlserverflexbetaResGen.DatabaseResourceSchema(ctx) + + fields, err := utils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return + } + + err = utils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } func (r *databaseResource) IdentitySchema( diff --git a/stackit/internal/services/sqlserverflexbeta/instance/resource.go b/stackit/internal/services/sqlserverflexbeta/instance/resource.go index bdf07ed7..5ccfa3c0 100644 --- a/stackit/internal/services/sqlserverflexbeta/instance/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/instance/resource.go @@ -60,8 +60,24 @@ func (r *instanceResource) Metadata( resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_instance" } +//go:embed planModifiers.yaml +var modifiersFileByte []byte + func (r *instanceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = sqlserverflexbetaResGen.InstanceResourceSchema(ctx) + s := sqlserverflexbetaResGen.InstanceResourceSchema(ctx) + + fields, err := utils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return + } + + err = utils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } func (r *instanceResource) IdentitySchema( @@ -161,9 +177,6 @@ func (r *instanceResource) ModifyPlan( } } -//go:embed planModifiers.yaml -var modifiersFileByte []byte - func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data resourceModel crateErr := "[SQL Server Flex BETA - Create] error" diff --git a/stackit/internal/services/sqlserverflexbeta/user/planModifiers.yaml b/stackit/internal/services/sqlserverflexbeta/user/planModifiers.yaml new file mode 100644 index 00000000..fe4025ee --- /dev/null +++ b/stackit/internal/services/sqlserverflexbeta/user/planModifiers.yaml @@ -0,0 +1,51 @@ +fields: + - name: 'id' + modifiers: + - 'UseStateForUnknown' + + - name: 'user_id' + modifiers: + - 'UseStateForUnknown' + + - name: 'instance_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'UseStateForUnknown' + + - name: 'project_id' + validators: + - validate.NoSeparator + - validate.UUID + modifiers: + - 'UseStateForUnknown' + - 'RequiresReplace' + + - name: 'region' + modifiers: + - 'RequiresReplace' + + - name: 'user_id' + modifiers: + - 'RequiresReplace' + + - name: 'username' + modifiers: + - 'UseStateForUnknown' + + - name: 'roles' + modifiers: + - 'UseStateForUnknown' + + - name: 'password' + modifiers: + - 'UseStateForUnknown' + + - name: 'uri' + modifiers: + - 'UseStateForUnknown' + + - name: 'status' + modifiers: + - 'UseStateForUnknown' diff --git a/stackit/internal/services/sqlserverflexbeta/user/resource.go b/stackit/internal/services/sqlserverflexbeta/user/resource.go index c196c75a..40f78c7f 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/resource.go @@ -2,6 +2,7 @@ package sqlserverflexbeta import ( "context" + _ "embed" "fmt" "strconv" "strings" @@ -12,9 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" - "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/pkg_gen/sqlserverflexbeta" + "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/conversion" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core" "tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/utils" @@ -54,8 +54,25 @@ func (r *userResource) Metadata(ctx context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_sqlserverflexbeta_user" } -func (r *userResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = sqlserverflexbetaResGen.UserResourceSchema(ctx) +//go:embed planModifiers.yaml +var modifiersFileByte []byte + +func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + + s := sqlserverflexbetaResGen.UserResourceSchema(ctx) + + fields, err := utils.ReadModifiersConfig(modifiersFileByte) + if err != nil { + resp.Diagnostics.AddError("error during read modifiers config file", err.Error()) + return + } + + err = utils.AddPlanModifiersToResourceSchema(fields, &s) + if err != nil { + resp.Diagnostics.AddError("error adding plan modifiers", err.Error()) + return + } + resp.Schema = s } func (r *userResource) IdentitySchema( -- 2.49.1