diff --git a/.gitignore b/.gitignore index c588a0f1..1dac2ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ dist pkg_gen /release/ +.env +**/.env diff --git a/docs/data-sources/sqlserverflexalpha_database.md b/docs/data-sources/sqlserverflexalpha_database.md index 20e8a653..df66ffb7 100644 --- a/docs/data-sources/sqlserverflexalpha_database.md +++ b/docs/data-sources/sqlserverflexalpha_database.md @@ -26,7 +26,7 @@ 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` (String) Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", +- `id` (String) The terraform internal identifier. - `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_flavor.md b/docs/data-sources/sqlserverflexalpha_flavor.md index 0dfc1fd2..7a03ecfb 100644 --- a/docs/data-sources/sqlserverflexalpha_flavor.md +++ b/docs/data-sources/sqlserverflexalpha_flavor.md @@ -29,20 +29,20 @@ data "stackitprivatepreview_sqlserverflexalpha_flavor" "flavor" { ### Required - `cpu` (Number) The cpu count of the instance. -- `node_type` (String) defines the nodeType it can be either single or replica -- `project_id` (String) The cpu count of the instance. +- `node_type` (String) defines the nodeType it can be either single or HA +- `project_id` (String) The project ID of the flavor. - `ram` (Number) The memory of the instance in Gibibyte. -- `region` (String) The flavor description. +- `region` (String) The region of the flavor. - `storage_class` (String) The memory of the instance in Gibibyte. ### Read-Only - `description` (String) The flavor description. -- `flavor_id` (String) The flavor id of the instance flavor. -- `id` (String) The terraform id of the instance flavor. +- `flavor_id` (String) The id of the instance flavor. +- `id` (String) The id of the instance flavor. - `max_gb` (Number) maximum storage which can be ordered for the flavor in Gigabyte. - `min_gb` (Number) minimum storage which is required to order in Gigabyte. -- `storage_classes` (Attributes List) (see [below for nested schema](#nestedatt--storage_classes)) +- `storage_classes` (Attributes List) maximum storage which can be ordered for the flavor in Gigabyte. (see [below for nested schema](#nestedatt--storage_classes)) ### Nested Schema for `storage_classes` diff --git a/docs/data-sources/sqlserverflexalpha_instance.md b/docs/data-sources/sqlserverflexalpha_instance.md index f209c424..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) Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\". - `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)) diff --git a/docs/data-sources/sqlserverflexalpha_user.md b/docs/data-sources/sqlserverflexalpha_user.md index b0b15341..63526135 100644 --- a/docs/data-sources/sqlserverflexalpha_user.md +++ b/docs/data-sources/sqlserverflexalpha_user.md @@ -3,12 +3,12 @@ page_title: "stackitprivatepreview_sqlserverflexalpha_user Data Source - stackitprivatepreview" subcategory: "" description: |- - SQLServer Flex user data source schema. Must have a region specified in the provider configuration. + --- # stackitprivatepreview_sqlserverflexalpha_user (Data Source) -SQLServer Flex user data source schema. Must have a `region` specified in the provider configuration. + ## Example Usage @@ -25,20 +25,38 @@ data "stackitprivatepreview_sqlserverflexalpha_user" "example" { ### Required -- `instance_id` (String) ID of the SQLServer Flex instance. -- `project_id` (String) STACKIT project ID to which the instance is associated. -- `user_id` (Number) User ID. +- `instance_id` (String) The ID of the instance. +- `project_id` (String) The STACKIT project ID. +- `region` (String) The region which should be addressed ### Optional -- `region` (String) The resource region. If not defined, the provider region is used. +- `page` (Number) Number of the page of items list to be returned. +- `size` (Number) Number of items to be returned on each page. +- `sort` (String) Sorting of the users to be returned on each page. ### Read-Only -- `default_database` (String) -- `host` (String) -- `id` (String) Terraform's internal data source. ID. It is structured as "`project_id`,`region`,`instance_id`,`user_id`". -- `port` (Number) -- `roles` (Set of String) Database access levels for the user. -- `status` (String) -- `username` (String) Username of the SQLServer Flex instance. +- `pagination` (Attributes) (see [below for nested schema](#nestedatt--pagination)) +- `users` (Attributes List) List of all users inside an instance (see [below for nested schema](#nestedatt--users)) + + +### Nested Schema for `pagination` + +Read-Only: + +- `page` (Number) +- `size` (Number) +- `sort` (String) +- `total_pages` (Number) +- `total_rows` (Number) + + + +### Nested Schema for `users` + +Read-Only: + +- `status` (String) The current status of the user. +- `tf_original_api_id` (Number) The ID of the user. +- `username` (String) The name of the user. diff --git a/docs/resources/postgresflexalpha_user.md b/docs/resources/postgresflexalpha_user.md index d68b920c..d0b9c500 100644 --- a/docs/resources/postgresflexalpha_user.md +++ b/docs/resources/postgresflexalpha_user.md @@ -44,7 +44,6 @@ import { ### Read-Only -- `connection_string` (String) The connection string for the user to the instance. - `id` (Number) The ID of the user. - `password` (String) The password for the user. - `status` (String) The current status of the user. diff --git a/sample/postgres/postresql.tf b/sample/postgres/postresql.tf index fa2f49e8..531b17e2 100644 --- a/sample/postgres/postresql.tf +++ b/sample/postgres/postresql.tf @@ -65,15 +65,15 @@ resource "stackitprivatepreview_postgresflexalpha_instance" "msh-sna-pe-example2 resource "stackitprivatepreview_postgresflexalpha_user" "ptlsdbadminuser" { project_id = var.project_id instance_id = stackitprivatepreview_postgresflexalpha_instance.msh-sna-pe-example.instance_id - username = var.db_admin_username - roles = ["createdb", "login"] + name = var.db_admin_username + roles = ["createdb", "login", "login"] # roles = ["createdb", "login", "createrole"] } resource "stackitprivatepreview_postgresflexalpha_user" "ptlsdbadminuser2" { project_id = var.project_id instance_id = stackitprivatepreview_postgresflexalpha_instance.msh-sna-pe-example2.instance_id - username = var.db_admin_username + name = var.db_admin_username roles = ["createdb", "login"] # roles = ["createdb", "login", "createrole"] } @@ -81,7 +81,7 @@ resource "stackitprivatepreview_postgresflexalpha_user" "ptlsdbadminuser2" { resource "stackitprivatepreview_postgresflexalpha_user" "ptlsdbuser" { project_id = var.project_id instance_id = stackitprivatepreview_postgresflexalpha_instance.msh-sna-pe-example.instance_id - username = var.db_username + name = var.db_name roles = ["login"] # roles = ["createdb", "login", "createrole"] } diff --git a/stackit/internal/services/postgresflexalpha/user/resource.go b/stackit/internal/services/postgresflexalpha/user/resource.go index 4adbaff0..cf9fd389 100644 --- a/stackit/internal/services/postgresflexalpha/user/resource.go +++ b/stackit/internal/services/postgresflexalpha/user/resource.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "math" + "slices" "strconv" "strings" "time" @@ -29,11 +30,12 @@ import ( var ( // Ensure the implementation satisfies the expected interfaces. - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} - _ resource.ResourceWithIdentity = &userResource{} + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} + _ resource.ResourceWithModifyPlan = &userResource{} + _ resource.ResourceWithIdentity = &userResource{} + _ resource.ResourceWithValidateConfig = &userResource{} // Error message constants extractErrorSummary = "extracting failed" @@ -138,6 +140,39 @@ func (r *userResource) Schema(ctx context.Context, _ resource.SchemaRequest, res resp.Schema = s } +func (r *userResource) 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() { + return + } + + var roles []string + diags := data.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + var resRoles []string + for _, role := range roles { + if slices.Contains(resRoles, role) { + resp.Diagnostics.AddAttributeError( + path.Root("roles"), + "Attribute Configuration Error", + "defined roles MUST NOT contain duplicates", + ) + return + } + resRoles = append(resRoles, role) + } +} + // Create creates the resource and sets the initial Terraform state. func (r *userResource) Create( ctx context.Context, @@ -217,7 +252,7 @@ func (r *userResource) Create( model.UserId = types.Int64Value(id) model.Password = types.StringValue(userResp.GetPassword()) model.Status = types.StringValue(userResp.GetStatus()) - model.ConnectionString = types.StringValue(userResp.GetConnectionString()) + //model.ConnectionString = types.StringValue(userResp.GetConnectionString()) waitResp, err := postgresflexalphaWait.GetUserByIdWaitHandler( ctx, @@ -712,5 +747,6 @@ func (r *userResource) expandRoles(ctx context.Context, rolesSet types.List, dia } var roles []string diags.Append(rolesSet.ElementsAs(ctx, &roles, false)...) + slices.Sort(roles) return roles } 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 f07ab701..f96d8d93 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 @@ -14,11 +14,6 @@ import ( func UserResourceSchema(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.", - }, "id": schema.Int64Attribute{ Computed: true, Description: "The ID of the user.", @@ -80,14 +75,13 @@ func UserResourceSchema(ctx context.Context) schema.Schema { } type UserModel struct { - ConnectionString types.String `tfsdk:"connection_string"` - Id types.Int64 `tfsdk:"id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - Password types.String `tfsdk:"password"` - 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:"id"` + InstanceId types.String `tfsdk:"instance_id"` + Name types.String `tfsdk:"name"` + Password types.String `tfsdk:"password"` + 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/sqlserverflexalpha/user/mapper.go b/stackit/internal/services/sqlserverflexalpha/user/mapper.go index 515c9ddc..8e522d59 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/mapper.go +++ b/stackit/internal/services/sqlserverflexalpha/user/mapper.go @@ -2,6 +2,7 @@ package sqlserverflexalpha import ( "fmt" + "slices" "strconv" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -44,8 +45,11 @@ func mapDataSourceFields(userResp *sqlserverflexalpha.GetUserResponse, model *da if user.Roles == nil { model.Roles = types.List(types.SetNull(types.StringType)) } else { + resRoles := *user.Roles + slices.Sort(resRoles) + var roles []attr.Value - for _, role := range *user.Roles { + for _, role := range resRoles { roles = append(roles, types.StringValue(string(role))) } rolesSet, diags := types.SetValue(types.StringType, roles) @@ -92,8 +96,11 @@ func mapFields(userResp *sqlserverflexalpha.GetUserResponse, model *resourceMode // Map roles if user.Roles != nil { + resRoles := *user.Roles + slices.Sort(resRoles) + var roles []attr.Value - for _, role := range *user.Roles { + for _, role := range resRoles { roles = append(roles, types.StringValue(string(role))) } rolesSet, diags := types.SetValue(types.StringType, roles) @@ -139,8 +146,11 @@ func mapFieldsCreate(userResp *sqlserverflexalpha.CreateUserResponse, model *res model.Password = types.StringValue(*user.Password) if user.Roles != nil { + resRoles := *user.Roles + slices.Sort(resRoles) + var roles []attr.Value - for _, role := range *user.Roles { + for _, role := range resRoles { roles = append(roles, types.StringValue(string(role))) } rolesSet, diags := types.SetValue(types.StringType, roles) diff --git a/stackit/internal/services/sqlserverflexalpha/user/resource.go b/stackit/internal/services/sqlserverflexalpha/user/resource.go index 2b98c94c..8a0c1df7 100644 --- a/stackit/internal/services/sqlserverflexalpha/user/resource.go +++ b/stackit/internal/services/sqlserverflexalpha/user/resource.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "slices" "strconv" "strings" "time" @@ -30,11 +31,12 @@ import ( ) var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} - _ resource.ResourceWithIdentity = &userResource{} + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} + _ resource.ResourceWithModifyPlan = &userResource{} + _ resource.ResourceWithIdentity = &userResource{} + _ resource.ResourceWithValidateConfig = &userResource{} ) func NewUserResource() resource.Resource { @@ -156,6 +158,39 @@ func (r *userResource) IdentitySchema( } } +func (r *userResource) 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() { + return + } + + var roles []string + diags := data.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + var resRoles []string + for _, role := range roles { + if slices.Contains(resRoles, role) { + resp.Diagnostics.AddAttributeError( + path.Root("roles"), + "Attribute Configuration Error", + "defined roles MUST NOT contain duplicates", + ) + return + } + resRoles = append(resRoles, role) + } +} + // Create creates the resource and sets the initial Terraform state. func (r *userResource) Create( ctx context.Context, @@ -186,6 +221,8 @@ func (r *userResource) Create( if resp.Diagnostics.HasError() { return } + + slices.Sort(roles) } // Generate API request body from model diff --git a/stackit/internal/services/sqlserverflexbeta/user/mapper.go b/stackit/internal/services/sqlserverflexbeta/user/mapper.go index 9115906f..3674ac0b 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/mapper.go +++ b/stackit/internal/services/sqlserverflexbeta/user/mapper.go @@ -2,6 +2,7 @@ package sqlserverflexbeta import ( "fmt" + "slices" "strconv" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -92,10 +93,14 @@ func mapFields(userResp *sqlserverflexbeta.GetUserResponse, model *resourceModel // Map roles if user.Roles != nil { + resRoles := *user.Roles + slices.Sort(resRoles) + var roles []attr.Value - for _, role := range *user.Roles { + for _, role := range resRoles { 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)) @@ -139,8 +144,11 @@ func mapFieldsCreate(userResp *sqlserverflexbeta.CreateUserResponse, model *reso model.Password = types.StringValue(*user.Password) if user.Roles != nil { + resRoles := *user.Roles + slices.Sort(resRoles) + var roles []attr.Value - for _, role := range *user.Roles { + for _, role := range resRoles { roles = append(roles, types.StringValue(string(role))) } rolesSet, diags := types.SetValue(types.StringType, roles) diff --git a/stackit/internal/services/sqlserverflexbeta/user/resource.go b/stackit/internal/services/sqlserverflexbeta/user/resource.go index 9508c743..8f19eb61 100644 --- a/stackit/internal/services/sqlserverflexbeta/user/resource.go +++ b/stackit/internal/services/sqlserverflexbeta/user/resource.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "slices" "strconv" "strings" "time" @@ -30,11 +31,12 @@ import ( ) var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} - _ resource.ResourceWithIdentity = &userResource{} + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} + _ resource.ResourceWithModifyPlan = &userResource{} + _ resource.ResourceWithIdentity = &userResource{} + _ resource.ResourceWithValidateConfig = &userResource{} ) func NewUserResource() resource.Resource { @@ -156,6 +158,39 @@ func (r *userResource) IdentitySchema( } } +func (r *userResource) 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() { + return + } + + var roles []string + diags := data.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + var resRoles []string + for _, role := range roles { + if slices.Contains(resRoles, role) { + resp.Diagnostics.AddAttributeError( + path.Root("roles"), + "Attribute Configuration Error", + "defined roles MUST NOT contain duplicates", + ) + return + } + resRoles = append(resRoles, role) + } +} + // Create creates the resource and sets the initial Terraform state. func (r *userResource) Create( ctx context.Context, @@ -186,6 +221,7 @@ func (r *userResource) Create( if resp.Diagnostics.HasError() { return } + slices.Sort(roles) } // Generate API request body from model @@ -379,7 +415,7 @@ func (r *userResource) Update( resp *resource.UpdateResponse, ) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "an SQL server user can not be updated, only created") } // Delete deletes the resource and removes the Terraform state on success.