diff --git a/docs/data-sources/mariadb_credential.md b/docs/data-sources/mariadb_credential.md index dc5de1367..d0e9bab69 100644 --- a/docs/data-sources/mariadb_credential.md +++ b/docs/data-sources/mariadb_credential.md @@ -29,11 +29,15 @@ data "stackit_mariadb_credential" "example" { - `instance_id` (String) ID of the MariaDB instance. - `project_id` (String) STACKIT project ID to which the instance is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `host` (String) - `hosts` (List of String) -- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`instance_id`,`credential_id`". +- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`region`,`instance_id`,`credential_id`". - `name` (String) - `password` (String, Sensitive) - `port` (Number) diff --git a/docs/data-sources/mariadb_instance.md b/docs/data-sources/mariadb_instance.md index be2553cc6..42743ea19 100644 --- a/docs/data-sources/mariadb_instance.md +++ b/docs/data-sources/mariadb_instance.md @@ -27,13 +27,17 @@ data "stackit_mariadb_instance" "example" { - `instance_id` (String) ID of the MariaDB instance. - `project_id` (String) STACKIT Project ID to which the instance is associated. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `cf_guid` (String) - `cf_organization_guid` (String) - `cf_space_guid` (String) - `dashboard_url` (String) -- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`instance_id`". +- `id` (String) Terraform's internal data source. identifier. It is structured as "`project_id`,`region`,`instance_id`". - `image_url` (String) - `name` (String) Instance name. - `parameters` (Attributes) (see [below for nested schema](#nestedatt--parameters)) diff --git a/docs/resources/mariadb_credential.md b/docs/resources/mariadb_credential.md index 417718bdb..eb3e1f403 100644 --- a/docs/resources/mariadb_credential.md +++ b/docs/resources/mariadb_credential.md @@ -42,6 +42,7 @@ resource "stackit_mariadb_credential" "example_rotate" { ### Optional +- `region` (String) The resource region. If not defined, the provider region is used. - `rotate_when_changed` (Map of String) A map of arbitrary key/value pairs that will force recreation of the resource when they change, enabling resource rotation based on external conditions such as a rotating timestamp. Changing this forces a new resource to be created. ### Read-Only @@ -49,7 +50,7 @@ resource "stackit_mariadb_credential" "example_rotate" { - `credential_id` (String) The credential's ID. - `host` (String) - `hosts` (List of String) -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`instance_id`,`credential_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`instance_id`,`credential_id`". - `name` (String) - `password` (String, Sensitive) - `port` (Number) @@ -66,6 +67,6 @@ In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https:// # Only use the import statement, if you want to import an existing mariadb credential import { to = stackit_mariadb_credential.import-example - id = "${var.project_id},${var.mariadb_instance_id},${var.mariadb_credential_id}" + id = "${var.project_id},${var.region},${var.mariadb_instance_id},${var.mariadb_credential_id}" } ``` diff --git a/docs/resources/mariadb_instance.md b/docs/resources/mariadb_instance.md index ddabdf615..5fd2d3f58 100644 --- a/docs/resources/mariadb_instance.md +++ b/docs/resources/mariadb_instance.md @@ -37,6 +37,7 @@ resource "stackit_mariadb_instance" "example" { ### Optional - `parameters` (Attributes) Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it. (see [below for nested schema](#nestedatt--parameters)) +- `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only @@ -44,7 +45,7 @@ resource "stackit_mariadb_instance" "example" { - `cf_organization_guid` (String) - `cf_space_guid` (String) - `dashboard_url` (String) -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`". +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`instance_id`". - `image_url` (String) - `instance_id` (String) ID of the MariaDB instance. - `plan_id` (String) The selected plan ID. @@ -73,6 +74,6 @@ In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https:// # Only use the import statement, if you want to import an existing mariadb instance import { to = stackit_mariadb_instance.import-example - id = "${var.project_id},${var.mariadb_instance_id}" + id = "${var.project_id},${var.region},${var.mariadb_instance_id}" } ``` diff --git a/examples/resources/stackit_mariadb_credential/import-by-string-id.tf b/examples/resources/stackit_mariadb_credential/import-by-string-id.tf index 8b00ef53b..c906a5e47 100644 --- a/examples/resources/stackit_mariadb_credential/import-by-string-id.tf +++ b/examples/resources/stackit_mariadb_credential/import-by-string-id.tf @@ -1,5 +1,5 @@ # Only use the import statement, if you want to import an existing mariadb credential import { to = stackit_mariadb_credential.import-example - id = "${var.project_id},${var.mariadb_instance_id},${var.mariadb_credential_id}" + id = "${var.project_id},${var.region},${var.mariadb_instance_id},${var.mariadb_credential_id}" } diff --git a/examples/resources/stackit_mariadb_instance/import-by-string-id.tf b/examples/resources/stackit_mariadb_instance/import-by-string-id.tf index e696a4e3f..0393edd03 100644 --- a/examples/resources/stackit_mariadb_instance/import-by-string-id.tf +++ b/examples/resources/stackit_mariadb_instance/import-by-string-id.tf @@ -1,5 +1,5 @@ # Only use the import statement, if you want to import an existing mariadb instance import { to = stackit_mariadb_instance.import-example - id = "${var.project_id},${var.mariadb_instance_id}" + id = "${var.project_id},${var.region},${var.mariadb_instance_id}" } diff --git a/go.mod b/go.mod index f530b0b66..e78461d68 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.15.0 github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1 github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0 - github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.0.1 + github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.1.0 github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.9.0 diff --git a/go.sum b/go.sum index 78bb4a31f..be2ed0801 100644 --- a/go.sum +++ b/go.sum @@ -700,8 +700,8 @@ github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1 h1:iteL61eMMPWT6872 github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1/go.mod h1:JDOOYaGgcBts2x52nKPRMFgSZe7qqOFmfz1xIXCQgRY= github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0 h1:g7zpfQFFq3UhAWrMK9rPaZY6dLMAuMJf5g6+r7VRTXc= github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0/go.mod h1:tvRejL8w5KpGBbLFPQ+dXOJURgZ3OMbZmwxlKQrGMuA= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.0.1 h1:Ll1erKKms7OnFva3bRXOs9PEYLoWJp5prSg985K0glk= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.0.1/go.mod h1:joa89Y1dyn0j22FstRcIKfW2ada3FDxNfttxSvq27uY= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.1.0 h1:3IZ3baztlqmkkDxwBfRymYTFeZa3AARVXYlt3hohfPs= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v1.1.0/go.mod h1:joa89Y1dyn0j22FstRcIKfW2ada3FDxNfttxSvq27uY= github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.11.0 h1:LfcQ++Z8a13jrJ5NOaCY/hwToh/e+QJj0eS6rd6s6k8= github.com/stackitcloud/stackit-sdk-go/services/modelserving v0.11.0/go.mod h1:u7T85YqoqncJevbPU1ODKthbmxxEh1zw+bVaAO8v0Sg= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0 h1:SVd3WMmLAE0Jxk2SaRuM85DTOOXHycyMpZGx9vzqNkI= diff --git a/stackit/internal/services/mariadb/credential/datasource.go b/stackit/internal/services/mariadb/credential/datasource.go index f8244cbf3..a2f3505f1 100644 --- a/stackit/internal/services/mariadb/credential/datasource.go +++ b/stackit/internal/services/mariadb/credential/datasource.go @@ -18,12 +18,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" ) // Ensure the implementation satisfies the expected interfaces. var ( - _ datasource.DataSource = &credentialDataSource{} + _ datasource.DataSource = &credentialDataSource{} + _ datasource.DataSourceWithConfigure = &credentialDataSource{} ) type DataSourceModel struct { @@ -38,6 +39,7 @@ type DataSourceModel struct { Port types.Int32 `tfsdk:"port"` Uri types.String `tfsdk:"uri"` Username types.String `tfsdk:"username"` + Region types.String `tfsdk:"region"` } // NewCredentialDataSource is a helper function to simplify the provider implementation. @@ -47,37 +49,40 @@ func NewCredentialDataSource() datasource.DataSource { // credentialDataSource is the data source implementation. type credentialDataSource struct { - client *mariadb.APIClient + client *mariadb.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_mariadb_credential" } // Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) +func (d *credentialDataSource) 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 } - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := mariadbUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - r.client = apiClient + d.client = apiClient tflog.Info(ctx, "mariadb credential client configured") } // Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ //nolint:gosec // description for credential id "main": "MariaDB credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", + "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`region`,`instance_id`,`credential_id`\".", "credential_id": "The credential's ID.", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT project ID to which the instance is associated.", + "region": "The resource region. If not defined, the provider region is used.", } resp.Schema = schema.Schema{ @@ -135,12 +140,18 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ "username": schema.StringAttribute{ 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"], + }, }, } } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *credentialDataSource) 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...) @@ -151,13 +162,15 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "credential_id", credentialId) - recordSetResp, err := r.client.DefaultAPI.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() + recordSetResp, err := d.client.DefaultAPI.GetCredentials(ctx, projectId, region, instanceId, credentialId).Execute() if err != nil { utils.LogError( ctx, @@ -176,7 +189,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapDataSourceFields(ctx, recordSetResp, &model) + err = mapDataSourceFields(ctx, recordSetResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -191,7 +204,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ tflog.Info(ctx, "mariadb credential read") } -func mapDataSourceFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *DataSourceModel) error { +func mapDataSourceFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *DataSourceModel, region string) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -214,9 +227,11 @@ func mapDataSourceFields(ctx context.Context, credentialsResp *mariadb.Credentia model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), + region, model.InstanceId.ValueString(), credentialId, ) + model.Region = types.StringValue(region) modelHosts, err := utils.ListValueToStringSlice(model.Hosts) if err != nil { diff --git a/stackit/internal/services/mariadb/credential/datasource_test.go b/stackit/internal/services/mariadb/credential/datasource_test.go index bbf487742..c2af9ad91 100644 --- a/stackit/internal/services/mariadb/credential/datasource_test.go +++ b/stackit/internal/services/mariadb/credential/datasource_test.go @@ -2,12 +2,13 @@ package mariadb import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" ) func TestMapDataSourceFields(t *testing.T) { @@ -29,7 +30,7 @@ func TestMapDataSourceFields(t *testing.T) { Raw: &mariadb.RawCredentials{}, }, DataSourceModel{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -40,6 +41,7 @@ func TestMapDataSourceFields(t *testing.T) { Port: types.Int32Null(), Uri: types.StringNull(), Username: types.StringValue(""), + Region: types.StringValue(testRegion), }, true, }, @@ -67,7 +69,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, DataSourceModel{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -81,6 +83,7 @@ func TestMapDataSourceFields(t *testing.T) { Port: types.Int32Value(1234), Uri: types.StringValue("uri"), Username: types.StringValue("username"), + Region: types.StringValue(testRegion), }, true, }, @@ -114,7 +117,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, DataSourceModel{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -129,6 +132,7 @@ func TestMapDataSourceFields(t *testing.T) { Port: types.Int32Value(1234), Uri: types.StringValue("uri"), Username: types.StringValue("username"), + Region: types.StringValue(testRegion), }, true, }, @@ -153,7 +157,7 @@ func TestMapDataSourceFields(t *testing.T) { }, }, DataSourceModel{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -164,6 +168,7 @@ func TestMapDataSourceFields(t *testing.T) { Port: types.Int32Value(2123456789), Uri: types.StringNull(), Username: types.StringValue(""), + Region: types.StringValue(testRegion), }, true, }, @@ -202,7 +207,7 @@ func TestMapDataSourceFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state) + err := mapDataSourceFields(context.Background(), tt.input, &tt.state, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/mariadb/credential/resource.go b/stackit/internal/services/mariadb/credential/resource.go index f7a355fa1..90f322a34 100644 --- a/stackit/internal/services/mariadb/credential/resource.go +++ b/stackit/internal/services/mariadb/credential/resource.go @@ -25,8 +25,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api/wait" ) // Ensure the implementation satisfies the expected interfaces. @@ -34,6 +34,7 @@ var ( _ resource.Resource = &credentialResource{} _ resource.ResourceWithConfigure = &credentialResource{} _ resource.ResourceWithImportState = &credentialResource{} + _ resource.ResourceWithModifyPlan = &credentialResource{} ) type Model struct { @@ -52,7 +53,8 @@ type Model struct { // recreation of the resource when they change, enabling resource rotation based on // external conditions such as a rotating timestamp. Changing this forces a new // resource to be created. - RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` + RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` + Region types.String `tfsdk:"region"` } // NewCredentialResource is a helper function to simplify the provider implementation. @@ -62,7 +64,8 @@ func NewCredentialResource() resource.Resource { // credentialResource is the resource implementation. type credentialResource struct { - client *mariadb.APIClient + client *mariadb.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -72,12 +75,13 @@ func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRe // Configure adds the provider configured client to the resource. func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := mariadbUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -85,14 +89,45 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu tflog.Info(ctx, "MariaDB credential client configured") } +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *credentialResource) 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 + } +} + // Schema defines the schema for the resource. func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ //nolint:gosec // description for credential id "main": "MariaDB credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`instance_id`,`credential_id`\".", "credential_id": "The credential's ID.", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT Project ID to which the instance is associated.", + "region": "The resource region. If not defined, the provider region is used.", } resp.Schema = schema.Schema{ @@ -176,6 +211,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, mapplanmodifier.RequiresReplace(), }, }, + "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(), + }, + }, }, } } @@ -192,12 +236,14 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) // Create new recordset - credentialsResp, err := r.client.DefaultAPI.CreateCredentials(ctx, projectId, instanceId).Execute() + credentialsResp, err := r.client.DefaultAPI.CreateCredentials(ctx, projectId, region, instanceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return @@ -209,6 +255,7 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, + "region": region, "instance_id": instanceId, "credential_id": credentialId, }) @@ -216,14 +263,14 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ return } - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, credentialId).WaitWithContext(ctx) + waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) return } // Map response body to schema - err = mapFields(ctx, waitResp, &model) + err = mapFields(ctx, waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -248,6 +295,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() if credentialId == "" { @@ -256,10 +304,11 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, return } ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "credential_id", credentialId) - recordSetResp, err := r.client.DefaultAPI.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() + recordSetResp, err := r.client.DefaultAPI.GetCredentials(ctx, projectId, region, instanceId, credentialId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { @@ -273,7 +322,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) + err = mapFields(ctx, recordSetResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -306,14 +355,16 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "credential_id", credentialId) // Delete existing record set - err := r.client.DefaultAPI.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() + err := r.client.DefaultAPI.DeleteCredentials(ctx, projectId, region, instanceId, credentialId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { @@ -325,7 +376,7 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ ctx = core.LogResponse(ctx) - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId, credentialId).WaitWithContext(ctx) + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return @@ -334,26 +385,27 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,region,instance_id,credential_id func (r *credentialResource) 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] == "" { + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), + fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[credential_id], got %q", req.ID), ) return } ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": idParts[0], - "instance_id": idParts[1], - "credential_id": idParts[2], + "region": idParts[1], + "instance_id": idParts[2], + "credential_id": idParts[3], }) tflog.Info(ctx, "MariaDB credential state imported") } -func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *Model) error { +func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *Model, region string) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -376,9 +428,11 @@ func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), + region, model.InstanceId.ValueString(), credentialId, ) + model.Region = types.StringValue(region) modelHosts, err := utils.ListValueToStringSlice(model.Hosts) if err != nil { diff --git a/stackit/internal/services/mariadb/credential/resource_test.go b/stackit/internal/services/mariadb/credential/resource_test.go index 97aefc43d..42edf76e9 100644 --- a/stackit/internal/services/mariadb/credential/resource_test.go +++ b/stackit/internal/services/mariadb/credential/resource_test.go @@ -2,14 +2,17 @@ package mariadb import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" ) +const testRegion = "eu02" + func TestMapFields(t *testing.T) { tests := []struct { description string @@ -30,7 +33,7 @@ func TestMapFields(t *testing.T) { Raw: &mariadb.RawCredentials{}, }, Model{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -42,6 +45,7 @@ func TestMapFields(t *testing.T) { Uri: types.StringNull(), Username: types.StringValue(""), RotateWhenChanged: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), }, true, }, @@ -70,7 +74,7 @@ func TestMapFields(t *testing.T) { }, }, Model{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -85,6 +89,7 @@ func TestMapFields(t *testing.T) { Uri: types.StringValue("uri"), Username: types.StringValue("username"), RotateWhenChanged: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), }, true, }, @@ -119,7 +124,7 @@ func TestMapFields(t *testing.T) { }, }, Model{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -135,6 +140,7 @@ func TestMapFields(t *testing.T) { Uri: types.StringValue("uri"), Username: types.StringValue("username"), RotateWhenChanged: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), }, true, }, @@ -160,7 +166,7 @@ func TestMapFields(t *testing.T) { }, }, Model{ - Id: types.StringValue("pid,iid,cid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), CredentialId: types.StringValue("cid"), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), @@ -172,6 +178,7 @@ func TestMapFields(t *testing.T) { Uri: types.StringNull(), Username: types.StringValue(""), RotateWhenChanged: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), }, true, }, @@ -213,7 +220,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) + err := mapFields(context.Background(), tt.input, &tt.state, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/mariadb/instance/datasource.go b/stackit/internal/services/mariadb/instance/datasource.go index 6c666af04..381982ffa 100644 --- a/stackit/internal/services/mariadb/instance/datasource.go +++ b/stackit/internal/services/mariadb/instance/datasource.go @@ -18,7 +18,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" ) // Ensure the implementation satisfies the expected interfaces. @@ -33,40 +33,43 @@ func NewInstanceDataSource() datasource.DataSource { // instanceDataSource is the data source implementation. type instanceDataSource struct { - client *mariadb.APIClient + client *mariadb.APIClient + providerData core.ProviderData } // Metadata returns the data source type name. -func (r *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 + "_mariadb_instance" } // Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) +func (d *instanceDataSource) 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 } - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := mariadbUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - r.client = apiClient + d.client = apiClient tflog.Info(ctx, "MariaDB instance client configured") } // Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ "main": "MariaDB instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", + "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`region`,`instance_id`\".", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT Project ID to which the instance is associated.", "name": "Instance name.", "version": "The service version.", "plan_name": "The selected plan name.", "plan_id": "The selected plan ID.", + "region": "The resource region. If not defined, the provider region is used.", } parametersDescriptions := map[string]string{ @@ -171,12 +174,18 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques "cf_organization_guid": schema.StringAttribute{ 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"], + }, }, } } // 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 +func (d *instanceDataSource) 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...) @@ -187,11 +196,13 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) - instanceResp, err := r.client.DefaultAPI.GetInstance(ctx, projectId, instanceId).Execute() + instanceResp, err := d.client.DefaultAPI.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { utils.LogError( ctx, @@ -210,14 +221,14 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques ctx = core.LogResponse(ctx) - err = mapFields(instanceResp, &model) + err = mapFields(instanceResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) + err = loadPlanNameAndVersion(ctx, d.client, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return diff --git a/stackit/internal/services/mariadb/instance/resource.go b/stackit/internal/services/mariadb/instance/resource.go index 51f763938..5de676dfc 100644 --- a/stackit/internal/services/mariadb/instance/resource.go +++ b/stackit/internal/services/mariadb/instance/resource.go @@ -27,8 +27,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api/wait" ) // Ensure the implementation satisfies the expected interfaces. @@ -36,6 +36,7 @@ var ( _ resource.Resource = &instanceResource{} _ resource.ResourceWithConfigure = &instanceResource{} _ resource.ResourceWithImportState = &instanceResource{} + _ resource.ResourceWithModifyPlan = &instanceResource{} ) type Model struct { @@ -52,6 +53,7 @@ type Model struct { Version types.String `tfsdk:"version"` PlanName types.String `tfsdk:"plan_name"` PlanId types.String `tfsdk:"plan_id"` + Region types.String `tfsdk:"region"` } // Struct corresponding to DataSourceModel.Parameters @@ -85,7 +87,8 @@ func NewInstanceResource() resource.Resource { // instanceResource is the resource implementation. type instanceResource struct { - client *mariadb.APIClient + client *mariadb.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -95,12 +98,13 @@ func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := mariadbUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -108,11 +112,41 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure tflog.Info(ctx, "MariaDB instance client configured") } +// 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 + 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 + } +} + // Schema defines the schema for the resource. func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "MariaDB instance 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`\".", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT project ID to which the instance is associated.", "name": "Instance name.", @@ -120,6 +154,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "plan_name": "The selected plan name.", "plan_id": "The selected plan ID.", "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", + "region": "The resource region. If not defined, the provider region is used.", } parametersDescriptions := map[string]string{ @@ -271,6 +306,15 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r stringplanmodifier.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(), + }, + }, }, } } @@ -287,7 +331,9 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) var parameters *parametersModel if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { @@ -299,7 +345,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } - err := r.loadPlanId(ctx, &model) + err := r.loadPlanId(ctx, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) return @@ -312,7 +358,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques return } // Create new instance - createResp, err := r.client.DefaultAPI.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() + createResp, err := r.client.DefaultAPI.CreateInstance(ctx, projectId, region).CreateInstancePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return @@ -329,6 +375,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, + "region": region, "instance_id": instanceId, }) if resp.Diagnostics.HasError() { @@ -336,14 +383,14 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId).WaitWithContext(ctx) + waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) return } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -370,6 +417,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() if instanceId == "" { // Resource not yet created; ID is unknown. @@ -377,9 +425,10 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) - instanceResp, err := r.client.DefaultAPI.GetInstance(ctx, projectId, instanceId).Execute() + instanceResp, err := r.client.DefaultAPI.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { @@ -393,14 +442,14 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(instanceResp, &model) + err = mapFields(instanceResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) + err = loadPlanNameAndVersion(ctx, r.client, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return @@ -427,8 +476,10 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel @@ -441,7 +492,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } - err := r.loadPlanId(ctx, &model) + err := r.loadPlanId(ctx, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) return @@ -454,7 +505,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques return } // Update existing instance - err = r.client.DefaultAPI.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() + err = r.client.DefaultAPI.PartialUpdateInstance(ctx, projectId, region, instanceId).PartialUpdateInstancePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return @@ -462,14 +513,14 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = core.LogResponse(ctx) - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId).WaitWithContext(ctx) + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) return } // Map response body to schema - err = mapFields(waitResp, &model) + err = mapFields(waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return @@ -496,12 +547,14 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) // Delete existing instance - err := r.client.DefaultAPI.DeleteInstance(ctx, projectId, instanceId).Execute() + err := r.client.DefaultAPI.DeleteInstance(ctx, projectId, region, instanceId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { @@ -514,7 +567,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques ctx = core.LogResponse(ctx) - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId).WaitWithContext(ctx) + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return @@ -523,26 +576,27 @@ 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,instance_id +// 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) { idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + 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],[instance_id] Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), ) return } ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": idParts[0], - "instance_id": idParts[1], + "region": idParts[1], + "instance_id": idParts[2], }) tflog.Info(ctx, "MariaDB instance state imported") } -func mapFields(instance *mariadb.Instance, model *Model) error { +func mapFields(instance *mariadb.Instance, model *Model, region string) error { if instance == nil { return fmt.Errorf("response input is nil") } @@ -559,7 +613,8 @@ func mapFields(instance *mariadb.Instance, model *Model) error { return fmt.Errorf("instance id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId) + model.Region = types.StringValue(region) model.InstanceId = types.StringValue(instanceId) model.PlanId = types.StringValue(instance.PlanId) model.CfGuid = types.StringValue(instance.CfGuid) @@ -724,9 +779,9 @@ func toInstanceParams(parameters *parametersModel) (*mariadb.InstanceParameters, return payloadParams, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model, region string) error { projectId := model.ProjectId.ValueString() - res, err := r.client.DefaultAPI.ListOfferings(ctx, projectId).Execute() + res, err := r.client.DefaultAPI.ListOfferings(ctx, projectId, region).Execute() if err != nil { return fmt.Errorf("getting MariaDB offerings: %w", err) } @@ -761,10 +816,10 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } -func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, model *Model) error { +func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, model *Model, region string) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() - res, err := client.DefaultAPI.ListOfferings(ctx, projectId).Execute() + res, err := client.DefaultAPI.ListOfferings(ctx, projectId, region).Execute() if err != nil { return fmt.Errorf("getting MariaDB offerings: %w", err) } diff --git a/stackit/internal/services/mariadb/instance/resource_test.go b/stackit/internal/services/mariadb/instance/resource_test.go index cced777f4..715d9408d 100644 --- a/stackit/internal/services/mariadb/instance/resource_test.go +++ b/stackit/internal/services/mariadb/instance/resource_test.go @@ -2,15 +2,18 @@ package mariadb import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" ) +const testRegion = "eu02" + var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ "sgw_acl": types.StringValue("acl"), "enable_monitoring": types.BoolValue(true), @@ -58,7 +61,7 @@ func TestMapFields(t *testing.T) { "default_values", &mariadb.Instance{}, Model{ - Id: types.StringValue("pid,iid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid", testRegion)), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), PlanId: types.StringValue(""), @@ -69,6 +72,7 @@ func TestMapFields(t *testing.T) { ImageUrl: types.StringValue(""), CfOrganizationGuid: types.StringValue(""), Parameters: types.ObjectNull(parametersTypes), + Region: types.StringValue(testRegion), }, true, }, @@ -95,7 +99,7 @@ func TestMapFields(t *testing.T) { }, }, Model{ - Id: types.StringValue("pid,iid"), + Id: types.StringValue(fmt.Sprintf("pid,%s,iid", testRegion)), InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), PlanId: types.StringValue("plan"), @@ -106,6 +110,7 @@ func TestMapFields(t *testing.T) { ImageUrl: types.StringValue("image"), CfOrganizationGuid: types.StringValue("org"), Parameters: fixtureModelParameters, + Region: types.StringValue(testRegion), }, true, }, @@ -148,7 +153,7 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } - err := mapFields(tt.input, state) + err := mapFields(tt.input, state, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/mariadb/mariadb_acc_test.go b/stackit/internal/services/mariadb/mariadb_acc_test.go index 8ca209c2c..463d8670e 100644 --- a/stackit/internal/services/mariadb/mariadb_acc_test.go +++ b/stackit/internal/services/mariadb/mariadb_acc_test.go @@ -13,8 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/utils" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" @@ -30,7 +30,7 @@ var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), "plan_name": config.StringVariable("stackit-mariadb-1.4.10-single"), - "db_version": config.StringVariable("10.6"), + "db_version": config.StringVariable("10.11"), } var testConfigVarsMax = config.Variables{ @@ -159,11 +159,15 @@ func TestAccMariaDbResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_mariadb_instance.instance") } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } instanceId, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, instanceId), nil }, ImportState: true, ImportStateVerify: true, @@ -176,6 +180,10 @@ func TestAccMariaDbResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_mariadb_credential.credential") } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } instanceId, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") @@ -184,7 +192,7 @@ func TestAccMariaDbResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, region, instanceId, credentialId), nil }, ImportState: true, ImportStateVerify: true, @@ -320,11 +328,15 @@ func TestAccMariaDbResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_mariadb_instance.instance") } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } instanceId, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, instanceId), nil }, ImportState: true, ImportStateVerify: true, @@ -337,6 +349,10 @@ func TestAccMariaDbResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_mariadb_credential.credential") } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } instanceId, ok := r.Primary.Attributes["instance_id"] if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") @@ -345,7 +361,7 @@ func TestAccMariaDbResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, region, instanceId, credentialId), nil }, ImportState: true, ImportStateVerify: true, @@ -408,7 +424,7 @@ func TestAccMariaDbResourceMax(t *testing.T) { func testAccCheckMariaDBDestroy(s *terraform.State) error { ctx := context.Background() - client, err := mariadb.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.MariaDBCustomEndpoint, true)...) + client, err := mariadb.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.MariaDBCustomEndpoint, false)...) if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -418,12 +434,12 @@ func testAccCheckMariaDBDestroy(s *terraform.State) error { if rs.Type != "stackit_mariadb_instance" { continue } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] + // instance terraform ID: "[project_id],[region],[instance_id]" + instanceId := strings.Split(rs.Primary.ID, core.Separator)[2] instancesToDestroy = append(instancesToDestroy, instanceId) } - instancesResp, err := client.DefaultAPI.ListInstances(ctx, testutil.ProjectId).Execute() + instancesResp, err := client.DefaultAPI.ListInstances(ctx, testutil.ProjectId, testutil.Region).Execute() if err != nil { return fmt.Errorf("getting instancesResp: %w", err) } @@ -435,11 +451,11 @@ func testAccCheckMariaDBDestroy(s *terraform.State) error { } if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DefaultAPI.DeleteInstance(ctx, testutil.ProjectId, *instances[i].InstanceId).Execute() + err := client.DefaultAPI.DeleteInstance(ctx, testutil.ProjectId, testutil.Region, *instances[i].InstanceId).Execute() if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } - _, err = wait.DeleteInstanceWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) + _, err = wait.DeleteInstanceWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, testutil.Region, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) } diff --git a/stackit/internal/services/mariadb/mariadb_test.go b/stackit/internal/services/mariadb/mariadb_test.go index 2a1e93548..eaf8c5b80 100644 --- a/stackit/internal/services/mariadb/mariadb_test.go +++ b/stackit/internal/services/mariadb/mariadb_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -87,7 +87,7 @@ resource "stackit_mariadb_instance" "example" { testutil.MockResponse{ Description: "refresh", Handler: func(w http.ResponseWriter, req *http.Request) { - expected := fmt.Sprintf("/v1/projects/%s/instances/%s", projectId, instanceId) + expected := fmt.Sprintf("/v2/projects/%s/regions/%s/instances/%s", projectId, region, instanceId) if req.URL.Path != expected { t.Errorf("expected request to %s, got %s", expected, req.URL.Path) } @@ -157,7 +157,7 @@ resource "stackit_mariadb_credential" "example" { testutil.MockResponse{ Description: "refresh", Handler: func(w http.ResponseWriter, req *http.Request) { - expected := fmt.Sprintf("/v1/projects/%s/instances/%s/credentials/%s", projectId, instanceId, credentialId) + expected := fmt.Sprintf("/v2/projects/%s/regions/%s/instances/%s/credentials/%s", projectId, region, instanceId, credentialId) if req.URL.Path != expected { t.Errorf("expected request to %s, got %s", expected, req.URL.Path) } diff --git a/stackit/internal/services/mariadb/utils/util.go b/stackit/internal/services/mariadb/utils/util.go index 0293947c7..f0217eb0a 100644 --- a/stackit/internal/services/mariadb/utils/util.go +++ b/stackit/internal/services/mariadb/utils/util.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" @@ -19,9 +19,8 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } if providerData.MariaDBCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.MariaDBCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := mariadb.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/mariadb/utils/util_test.go b/stackit/internal/services/mariadb/utils/util_test.go index 8ae0d8bb9..3d3223c2f 100644 --- a/stackit/internal/services/mariadb/utils/util_test.go +++ b/stackit/internal/services/mariadb/utils/util_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" - mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v2api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" @@ -46,7 +46,6 @@ func TestConfigureClient(t *testing.T) { }, expected: func() *mariadb.APIClient { apiClient, err := mariadb.NewAPIClient( - config.WithRegion("eu01"), utils.UserAgentConfigOption(testVersion), ) if err != nil {