From 932fff622328d6de6f33a3812e986af139ba4b5f Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:09:55 +0200 Subject: [PATCH] Onboard SQLServer Flex user resource (#403) * Onboard SQLServer Flex user resource * change roles * fix unit tests * make database field optional // adapt test and documentation * add sleep time to instance creation * fix service name in logs and descriptions * extend username plan modifiers * update docs * remove database field * remove database // make roles optional * update docs --- docs/data-sources/sqlserverflex_user.md | 38 ++ docs/resources/sqlserverflex_user.md | 44 ++ .../stackit_sqlserverflex_user/data-source.tf | 5 + .../stackit_sqlserverflex_user/resource.tf | 7 + .../sqlserverflex/instance/resource.go | 5 + .../sqlserverflex/sqlserverflex_acc_test.go | 72 ++- .../services/sqlserverflex/user/datasource.go | 232 +++++++++ .../sqlserverflex/user/datasource_test.go | 133 ++++++ .../services/sqlserverflex/user/resource.go | 442 ++++++++++++++++++ .../sqlserverflex/user/resource_test.go | 364 +++++++++++++++ stackit/provider.go | 3 + 11 files changed, 1343 insertions(+), 2 deletions(-) create mode 100644 docs/data-sources/sqlserverflex_user.md create mode 100644 docs/resources/sqlserverflex_user.md create mode 100644 examples/data-sources/stackit_sqlserverflex_user/data-source.tf create mode 100644 examples/resources/stackit_sqlserverflex_user/resource.tf create mode 100644 stackit/internal/services/sqlserverflex/user/datasource.go create mode 100644 stackit/internal/services/sqlserverflex/user/datasource_test.go create mode 100644 stackit/internal/services/sqlserverflex/user/resource.go create mode 100644 stackit/internal/services/sqlserverflex/user/resource_test.go diff --git a/docs/data-sources/sqlserverflex_user.md b/docs/data-sources/sqlserverflex_user.md new file mode 100644 index 00000000..edb5c47f --- /dev/null +++ b/docs/data-sources/sqlserverflex_user.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_sqlserverflex_user Data Source - stackit" +subcategory: "" +description: |- + SQLServer Flex user data source schema. Must have a region specified in the provider configuration. +--- + +# stackit_sqlserverflex_user (Data Source) + +SQLServer Flex user data source schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +data "stackit_sqlserverflex_user" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + user_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `instance_id` (String) ID of the SQLServer Flex instance. +- `project_id` (String) STACKIT project ID to which the instance is associated. +- `user_id` (String) User ID. + +### Read-Only + +- `host` (String) +- `id` (String) Terraform's internal data source. ID. It is structured as "`project_id`,`instance_id`,`user_id`". +- `port` (Number) +- `roles` (Set of String) +- `username` (String) diff --git a/docs/resources/sqlserverflex_user.md b/docs/resources/sqlserverflex_user.md new file mode 100644 index 00000000..dc3d0c21 --- /dev/null +++ b/docs/resources/sqlserverflex_user.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_sqlserverflex_user Resource - stackit" +subcategory: "" +description: |- + [Warning: BETA] SQLServer Flex user resource schema. Must have a region specified in the provider configuration. +--- + +# stackit_sqlserverflex_user (Resource) + +[Warning: BETA] SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration. + +## Example Usage + +```terraform +resource "stackit_sqlserverflex_user" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + username = "username" + roles = ["role"] + database = "database" +} +``` + + +## Schema + +### Required + +- `instance_id` (String) ID of the SQLServer Flex instance. +- `project_id` (String) STACKIT project ID to which the instance is associated. +- `username` (String) + +### Optional + +- `roles` (Set of String) + +### Read-Only + +- `host` (String) +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`,`user_id`". +- `password` (String, Sensitive) +- `port` (Number) +- `user_id` (String) User ID. diff --git a/examples/data-sources/stackit_sqlserverflex_user/data-source.tf b/examples/data-sources/stackit_sqlserverflex_user/data-source.tf new file mode 100644 index 00000000..39e44e42 --- /dev/null +++ b/examples/data-sources/stackit_sqlserverflex_user/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_sqlserverflex_user" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + user_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_sqlserverflex_user/resource.tf b/examples/resources/stackit_sqlserverflex_user/resource.tf new file mode 100644 index 00000000..85ec0611 --- /dev/null +++ b/examples/resources/stackit_sqlserverflex_user/resource.tf @@ -0,0 +1,7 @@ +resource "stackit_sqlserverflex_user" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + username = "username" + roles = ["role"] + database = "database" +} diff --git a/stackit/internal/services/sqlserverflex/instance/resource.go b/stackit/internal/services/sqlserverflex/instance/resource.go index 7c32bba8..9448d6f9 100644 --- a/stackit/internal/services/sqlserverflex/instance/resource.go +++ b/stackit/internal/services/sqlserverflex/instance/resource.go @@ -397,6 +397,11 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } + + // After the instance creation, database might not be ready to accept connections immediately. + // That is why we add a sleep + time.Sleep(120 * time.Second) + tflog.Info(ctx, "SQLServer Flex instance created") } diff --git a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go index a727477b..3305b06a 100644 --- a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go +++ b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go @@ -9,9 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -36,6 +35,13 @@ var instanceResource = map[string]string{ "backup_schedule_updated": "00 12 * * *", } +// User resource data +var userResource = map[string]string{ + "username": fmt.Sprintf("tf-acc-user-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha)), + "role": "##STACKIT_LoginManager##", + "project_id": instanceResource["project_id"], +} + func configResources(backupSchedule string) string { return fmt.Sprintf(` %s @@ -58,6 +64,13 @@ func configResources(backupSchedule string) string { } backup_schedule = "%s" } + + resource "stackit_sqlserverflex_user" "user" { + project_id = stackit_sqlserverflex_instance.instance.project_id + instance_id = stackit_sqlserverflex_instance.instance.instance_id + username = "%s" + roles = ["%s"] + } `, testutil.SQLServerFlexProviderConfig(), instanceResource["project_id"], @@ -70,6 +83,8 @@ func configResources(backupSchedule string) string { instanceResource["version"], instanceResource["options_retention_days"], backupSchedule, + userResource["username"], + userResource["role"], ) } @@ -98,6 +113,17 @@ func TestAccSQLServerFlexResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "version", instanceResource["version"]), resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", instanceResource["options_retention_days"]), resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), + // User + resource.TestCheckResourceAttrPair( + "stackit_sqlserverflex_user.user", "project_id", + "stackit_sqlserverflex_instance.instance", "project_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_sqlserverflex_user.user", "instance_id", + "stackit_sqlserverflex_instance.instance", "instance_id", + ), + resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "user_id"), + resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "password"), ), }, // data source @@ -109,6 +135,12 @@ func TestAccSQLServerFlexResource(t *testing.T) { project_id = stackit_sqlserverflex_instance.instance.project_id instance_id = stackit_sqlserverflex_instance.instance.instance_id } + + data "stackit_sqlserverflex_user" "user" { + project_id = stackit_sqlserverflex_instance.instance.project_id + instance_id = stackit_sqlserverflex_instance.instance.instance_id + user_id = stackit_sqlserverflex_user.user.user_id + } `, configResources(instanceResource["backup_schedule"]), ), @@ -124,6 +156,11 @@ func TestAccSQLServerFlexResource(t *testing.T) { "data.stackit_sqlserverflex_instance.instance", "instance_id", "stackit_sqlserverflex_instance.instance", "instance_id", ), + resource.TestCheckResourceAttrPair( + "data.stackit_sqlserverflex_user.user", "instance_id", + "stackit_sqlserverflex_user.user", "instance_id", + ), + resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "acl.#", "1"), resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "acl.0", instanceResource["acl"]), resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.id", instanceResource["flavor_id"]), @@ -133,6 +170,15 @@ func TestAccSQLServerFlexResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "replicas", instanceResource["replicas"]), resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", instanceResource["options_retention_days"]), resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), + + // User data + resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "project_id", userResource["project_id"]), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "user_id"), + resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "username", userResource["username"]), + resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.#", "1"), + resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.0", userResource["role"]), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "host"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "port"), ), }, // Import @@ -163,6 +209,28 @@ func TestAccSQLServerFlexResource(t *testing.T) { return nil }, }, + { + ResourceName: "stackit_sqlserverflex_user.user", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_sqlserverflex_user.user"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_sqlserverflex_user.user") + } + instanceId, ok := r.Primary.Attributes["instance_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute instance_id") + } + userId, ok := r.Primary.Attributes["user_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute user_id") + } + + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, userId), nil + }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + }, // Update { Config: configResources(instanceResource["backup_schedule_updated"]), diff --git a/stackit/internal/services/sqlserverflex/user/datasource.go b/stackit/internal/services/sqlserverflex/user/datasource.go new file mode 100644 index 00000000..0ef1f451 --- /dev/null +++ b/stackit/internal/services/sqlserverflex/user/datasource.go @@ -0,0 +1,232 @@ +package sqlserverflex + +import ( + "context" + "fmt" + "net/http" + "strings" + + "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-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &userDataSource{} +) + +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + UserId types.String `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"` +} + +// 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 *sqlserverflex.APIClient +} + +// Metadata returns the data source type name. +func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sqlserverflex_user" +} + +// Configure adds the provider configured client to the data source. +func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + var apiClient *sqlserverflex.APIClient + var err error + if providerData.SQLServerFlexCustomEndpoint != "" { + apiClient, err = sqlserverflex.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.SQLServerFlexCustomEndpoint), + ) + } else { + apiClient, err = sqlserverflex.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err)) + return + } + + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex user client configured") +} + +// Schema defines the schema for the data source. +func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "SQLServer 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`,`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.", + } + + 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{ + Computed: true, + }, + "roles": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "host": schema.StringAttribute{ + Computed: true, + }, + "port": schema.Int64Attribute{ + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *userDataSource) 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() { + return + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema and populate Computed attribute values + err = mapDataSourceFields(recordSetResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", 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, "SQLServer Flex user read") +} + +func mapDataSourceFields(userResp *sqlserverflex.GetUserResponse, model *DataSourceModel) error { + if userResp == nil || userResp.Item == nil { + return fmt.Errorf("response is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + user := userResp.Item + + var userId string + if model.UserId.ValueString() != "" { + userId = model.UserId.ValueString() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + idParts := []string{ + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + userId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.UserId = types.StringValue(userId) + model.Username = types.StringPointerValue(user.Username) + + if user.Roles == nil { + model.Roles = types.SetNull(types.StringType) + } else { + roles := []attr.Value{} + for _, role := range *user.Roles { + roles = append(roles, types.StringValue(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.Host = types.StringPointerValue(user.Host) + model.Port = types.Int64PointerValue(user.Port) + return nil +} diff --git a/stackit/internal/services/sqlserverflex/user/datasource_test.go b/stackit/internal/services/sqlserverflex/user/datasource_test.go new file mode 100644 index 00000000..7a0b1511 --- /dev/null +++ b/stackit/internal/services/sqlserverflex/user/datasource_test.go @@ -0,0 +1,133 @@ +package sqlserverflex + +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" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + description string + input *sqlserverflex.GetUserResponse + expected DataSourceModel + isValid bool + }{ + { + "default_values", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{}, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Username: types.StringNull(), + Roles: types.SetNull(types.StringType), + Host: types.StringNull(), + Port: types.Int64Null(), + }, + true, + }, + { + "simple_values", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{ + Roles: &[]string{ + "role_1", + "role_2", + "", + }, + Username: utils.Ptr("username"), + Host: utils.Ptr("host"), + Port: utils.Ptr(int64(1234)), + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + 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), + }, + true, + }, + { + "null_fields_and_int_conversions", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{ + Id: utils.Ptr("uid"), + Roles: &[]string{}, + Username: nil, + Host: nil, + Port: utils.Ptr(int64(2123456789)), + }, + }, + DataSourceModel{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + 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), + }, + true, + }, + { + "nil_response", + nil, + DataSourceModel{}, + false, + }, + { + "nil_response_2", + &sqlserverflex.GetUserResponse{}, + DataSourceModel{}, + false, + }, + { + "no_resource_id", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{}, + }, + 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) + 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/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go new file mode 100644 index 00000000..36a189da --- /dev/null +++ b/stackit/internal/services/sqlserverflex/user/resource.go @@ -0,0 +1,442 @@ +package sqlserverflex + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &userResource{} + _ resource.ResourceWithConfigure = &userResource{} + _ resource.ResourceWithImportState = &userResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + UserId types.String `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"` +} + +// NewUserResource is a helper function to simplify the provider implementation. +func NewUserResource() resource.Resource { + return &userResource{} +} + +// userResource is the resource implementation. +type userResource struct { + client *sqlserverflex.APIClient +} + +// Metadata returns the resource type name. +func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sqlserverflex_user" +} + +// Configure adds the provider configured client to the resource. +func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(core.ProviderData) + if !ok { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) + return + } + + var apiClient *sqlserverflex.APIClient + var err error + if providerData.SQLServerFlexCustomEndpoint != "" { + apiClient, err = sqlserverflex.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithEndpoint(providerData.SQLServerFlexCustomEndpoint), + ) + } else { + apiClient, err = sqlserverflex.NewAPIClient( + config.WithCustomAuth(providerData.RoundTripper), + config.WithRegion(providerData.Region), + ) + } + + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "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 + } + + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex user client configured") +} + +// Schema defines the schema for the resource. +func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "[Warning: BETA] 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`,`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.", + } + + 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.StringAttribute{ + Description: descriptions["user_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + validate.NoSeparator(), + }, + }, + "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{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "roles": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf("##STACKIT_LoginManager##", "##STACKIT_DatabaseManager##"), + ), + }, + }, + "password": schema.StringAttribute{ + Computed: true, + Sensitive: true, + }, + "host": schema.StringAttribute{ + Computed: true, + }, + "port": schema.Int64Attribute{ + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *userResource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + + var roles []sqlserverflex.Role + if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + diags = model.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + } + + // Generate API request body from model + payload, err := toCreatePayload(&model, roles) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) + return + } + // Create new user + userResp, err := r.client.CreateUser(ctx, projectId, instanceId).CreateUserPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) + return + } + if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") + return + } + userId := *userResp.Item.Id + ctx = tflog.SetField(ctx, "user_id", userId) + + // Map response body to schema + err = mapFieldsCreate(userResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", 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, "SQLServer Flex user created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *userResource) 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 + } + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + if ok && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(recordSetResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", 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, "SQLServer Flex user read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, 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") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() + userId := model.UserId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) + ctx = tflog.SetField(ctx, "user_id", userId) + + // Delete existing record set + err := r.client.DeleteUser(ctx, projectId, instanceId, userId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) + return + } + tflog.Info(ctx, "SQLServer Flex user 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 *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing user", + fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[user_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("instance_id"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[2])...) + core.LogAndAddWarning(ctx, &resp.Diagnostics, + "SQLServer Flex user imported with empty password", + "The user password is not imported as it is only available upon creation of a new user. The password field will be empty.", + ) + tflog.Info(ctx, "SQLServer Flex user state imported") +} + +func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model) error { + if userResp == nil || userResp.Item == nil { + return fmt.Errorf("response is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + user := userResp.Item + + if user.Id == nil { + return fmt.Errorf("user id not present") + } + userId := *user.Id + idParts := []string{ + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + userId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.UserId = types.StringValue(userId) + model.Username = types.StringPointerValue(user.Username) + + if user.Password == nil { + return fmt.Errorf("user password not present") + } + model.Password = types.StringValue(*user.Password) + + if user.Roles == nil { + model.Roles = types.SetNull(types.StringType) + } else { + roles := []attr.Value{} + for _, role := range *user.Roles { + roles = append(roles, types.StringValue(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.Host = types.StringPointerValue(user.Host) + model.Port = types.Int64PointerValue(user.Port) + return nil +} + +func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model) error { + if userResp == nil || userResp.Item == nil { + return fmt.Errorf("response is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + user := userResp.Item + + var userId string + if model.UserId.ValueString() != "" { + userId = model.UserId.ValueString() + } else if user.Id != nil { + userId = *user.Id + } else { + return fmt.Errorf("user id not present") + } + idParts := []string{ + model.ProjectId.ValueString(), + model.InstanceId.ValueString(), + userId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.UserId = types.StringValue(userId) + model.Username = types.StringPointerValue(user.Username) + + if user.Roles == nil { + model.Roles = types.SetNull(types.StringType) + } else { + roles := []attr.Value{} + for _, role := range *user.Roles { + roles = append(roles, types.StringValue(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.Host = types.StringPointerValue(user.Host) + model.Port = types.Int64PointerValue(user.Port) + return nil +} + +func toCreatePayload(model *Model, roles []sqlserverflex.Role) (*sqlserverflex.CreateUserPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + return &sqlserverflex.CreateUserPayload{ + Username: conversion.StringValueToPointer(model.Username), + Roles: &roles, + }, nil +} diff --git a/stackit/internal/services/sqlserverflex/user/resource_test.go b/stackit/internal/services/sqlserverflex/user/resource_test.go new file mode 100644 index 00000000..48c8616b --- /dev/null +++ b/stackit/internal/services/sqlserverflex/user/resource_test.go @@ -0,0 +1,364 @@ +package sqlserverflex + +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" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" +) + +func TestMapFieldsCreate(t *testing.T) { + tests := []struct { + description string + input *sqlserverflex.CreateUserResponse + expected Model + isValid bool + }{ + { + "default_values", + &sqlserverflex.CreateUserResponse{ + Item: &sqlserverflex.User{ + Id: utils.Ptr("uid"), + Password: utils.Ptr(""), + }, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Username: types.StringNull(), + Roles: types.SetNull(types.StringType), + Password: types.StringValue(""), + Host: types.StringNull(), + Port: types.Int64Null(), + }, + true, + }, + { + "simple_values", + &sqlserverflex.CreateUserResponse{ + Item: &sqlserverflex.User{ + Id: utils.Ptr("uid"), + Roles: &[]string{ + "role_1", + "role_2", + "", + }, + Username: utils.Ptr("username"), + Password: utils.Ptr("password"), + Host: utils.Ptr("host"), + Port: utils.Ptr(int64(1234)), + }, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + 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(""), + }), + Password: types.StringValue("password"), + Host: types.StringValue("host"), + Port: types.Int64Value(1234), + }, + true, + }, + { + "null_fields_and_int_conversions", + &sqlserverflex.CreateUserResponse{ + Item: &sqlserverflex.User{ + Id: utils.Ptr("uid"), + Roles: &[]string{}, + Username: nil, + Password: utils.Ptr(""), + Host: nil, + Port: utils.Ptr(int64(2123456789)), + }, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Username: types.StringNull(), + Roles: types.SetValueMust(types.StringType, []attr.Value{}), + Password: types.StringValue(""), + Host: types.StringNull(), + Port: types.Int64Value(2123456789), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + { + "nil_response_2", + &sqlserverflex.CreateUserResponse{}, + Model{}, + false, + }, + { + "no_resource_id", + &sqlserverflex.CreateUserResponse{ + Item: &sqlserverflex.User{}, + }, + Model{}, + false, + }, + { + "no_password", + &sqlserverflex.CreateUserResponse{ + Item: &sqlserverflex.User{ + Id: utils.Ptr("uid"), + }, + }, + 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 := mapFieldsCreate(tt.input, state) + 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) { + tests := []struct { + description string + input *sqlserverflex.GetUserResponse + expected Model + isValid bool + }{ + { + "default_values", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{}, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Username: types.StringNull(), + Roles: types.SetNull(types.StringType), + Host: types.StringNull(), + Port: types.Int64Null(), + }, + true, + }, + { + "simple_values", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{ + Roles: &[]string{ + "role_1", + "role_2", + "", + }, + Username: utils.Ptr("username"), + Host: utils.Ptr("host"), + Port: utils.Ptr(int64(1234)), + }, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + 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), + }, + true, + }, + { + "null_fields_and_int_conversions", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{ + Id: utils.Ptr("uid"), + Roles: &[]string{}, + Username: nil, + Host: nil, + Port: utils.Ptr(int64(2123456789)), + }, + }, + Model{ + Id: types.StringValue("pid,iid,uid"), + UserId: types.StringValue("uid"), + 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), + }, + true, + }, + { + "nil_response", + nil, + Model{}, + false, + }, + { + "nil_response_2", + &sqlserverflex.GetUserResponse{}, + Model{}, + false, + }, + { + "no_resource_id", + &sqlserverflex.GetUserResponse{ + Item: &sqlserverflex.InstanceResponseUser{}, + }, + 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) + 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 []sqlserverflex.Role + expected *sqlserverflex.CreateUserPayload + isValid bool + }{ + { + "default_values", + &Model{}, + []sqlserverflex.Role{}, + &sqlserverflex.CreateUserPayload{ + Roles: &[]sqlserverflex.Role{}, + Username: nil, + }, + true, + }, + { + "default_values", + &Model{ + Username: types.StringValue("username"), + }, + []sqlserverflex.Role{ + "role_1", + "role_2", + }, + &sqlserverflex.CreateUserPayload{ + Roles: &[]sqlserverflex.Role{ + "role_1", + "role_2", + }, + Username: utils.Ptr("username"), + }, + true, + }, + { + "null_fields_and_int_conversions", + &Model{ + Username: types.StringNull(), + }, + []sqlserverflex.Role{ + "", + }, + &sqlserverflex.CreateUserPayload{ + Roles: &[]sqlserverflex.Role{ + "", + }, + Username: nil, + }, + true, + }, + { + "nil_model", + nil, + []sqlserverflex.Role{}, + nil, + false, + }, + { + "nil_roles", + &Model{ + Username: types.StringValue("username"), + }, + []sqlserverflex.Role{}, + &sqlserverflex.CreateUserPayload{ + Roles: &[]sqlserverflex.Role{}, + Username: utils.Ptr("username"), + }, + true, + }, + } + 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) + } + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index e07f5bd0..c76b913d 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -44,6 +44,7 @@ import ( skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" skeProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/project" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" + sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" sdkauth "github.com/stackitcloud/stackit-sdk-go/core/auth" "github.com/stackitcloud/stackit-sdk-go/core/config" @@ -398,6 +399,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource secretsManagerInstance.NewInstanceDataSource, secretsManagerUser.NewUserDataSource, sqlServerFlexInstance.NewInstanceDataSource, + sqlServerFlexUser.NewUserDataSource, skeProject.NewProjectDataSource, skeCluster.NewClusterDataSource, } @@ -438,6 +440,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { secretsManagerInstance.NewInstanceResource, secretsManagerUser.NewUserResource, sqlServerFlexInstance.NewInstanceResource, + sqlServerFlexUser.NewUserResource, skeProject.NewProjectResource, skeCluster.NewClusterResource, skeKubeconfig.NewKubeconfigResource,