From ae85b72f1a46003e2d51e378296dc17fa1f51d0d Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Fri, 31 Jul 2026 10:22:56 +0200 Subject: [PATCH] feat(logme): Region adjustment STACKITTPR-710 Signed-off-by: Alexander Dahmen --- docs/data-sources/logme_credential.md | 6 +- docs/data-sources/logme_instance.md | 6 +- docs/resources/logme_credential.md | 8 +- docs/resources/logme_instance.md | 5 +- .../import-by-string-id.tf | 2 +- .../import-by-string-id.tf | 2 +- go.mod | 2 +- go.sum | 4 +- .../services/logme/credential/datasource.go | 35 ++++-- .../services/logme/credential/resource.go | 93 ++++++++++---- .../logme/credential/resource_test.go | 16 ++- .../services/logme/instance/datasource.go | 37 ++++-- .../services/logme/instance/resource.go | 113 +++++++++++++----- .../services/logme/instance/resource_test.go | 13 +- .../internal/services/logme/logme_acc_test.go | 40 +++++-- stackit/internal/services/logme/utils/util.go | 5 +- .../services/logme/utils/util_test.go | 3 +- 17 files changed, 279 insertions(+), 111 deletions(-) diff --git a/docs/data-sources/logme_credential.md b/docs/data-sources/logme_credential.md index 147cd6cc9..08fac7d2d 100644 --- a/docs/data-sources/logme_credential.md +++ b/docs/data-sources/logme_credential.md @@ -29,10 +29,14 @@ data "stackit_logme_credential" "example" { - `instance_id` (String) ID of the LogMe 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) -- `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`". - `password` (String, Sensitive) - `port` (Number) - `uri` (String, Sensitive) diff --git a/docs/data-sources/logme_instance.md b/docs/data-sources/logme_instance.md index bd29872c8..a5c2df2d5 100644 --- a/docs/data-sources/logme_instance.md +++ b/docs/data-sources/logme_instance.md @@ -27,13 +27,17 @@ data "stackit_logme_instance" "example" { - `instance_id` (String) ID of the LogMe 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/logme_credential.md b/docs/resources/logme_credential.md index c30a90053..ca1a2ad54 100644 --- a/docs/resources/logme_credential.md +++ b/docs/resources/logme_credential.md @@ -27,11 +27,15 @@ resource "stackit_logme_credential" "example" { - `instance_id` (String) ID of the LogMe 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 - `credential_id` (String) The credential's ID. - `host` (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`". - `password` (String, Sensitive) - `port` (Number) - `uri` (String, Sensitive) @@ -47,6 +51,6 @@ In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https:// # Only use the import statement, if you want to import an existing logme credential import { to = stackit_logme_credential.import-example - id = "${var.project_id},${var.logme_instance_id},${var.logme_credentials_id}" + id = "${var.project_id},${var.region},${var.logme_instance_id},${var.logme_credentials_id}" } ``` diff --git a/docs/resources/logme_instance.md b/docs/resources/logme_instance.md index 698d07d66..bd8131e48 100644 --- a/docs/resources/logme_instance.md +++ b/docs/resources/logme_instance.md @@ -37,6 +37,7 @@ resource "stackit_logme_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_logme_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 LogMe instance. - `plan_id` (String) The selected plan ID. @@ -87,6 +88,6 @@ In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https:// # Only use the import statement, if you want to import an existing logme instance import { to = stackit_logme_instance.import-example - id = "${var.project_id},${var.logme_instance_id}" + id = "${var.project_id},${var.region},${var.logme_instance_id}" } ``` diff --git a/examples/resources/stackit_logme_credential/import-by-string-id.tf b/examples/resources/stackit_logme_credential/import-by-string-id.tf index 757c244cf..a5f4e4e0f 100644 --- a/examples/resources/stackit_logme_credential/import-by-string-id.tf +++ b/examples/resources/stackit_logme_credential/import-by-string-id.tf @@ -1,5 +1,5 @@ # Only use the import statement, if you want to import an existing logme credential import { to = stackit_logme_credential.import-example - id = "${var.project_id},${var.logme_instance_id},${var.logme_credentials_id}" + id = "${var.project_id},${var.region},${var.logme_instance_id},${var.logme_credentials_id}" } diff --git a/examples/resources/stackit_logme_instance/import-by-string-id.tf b/examples/resources/stackit_logme_instance/import-by-string-id.tf index ecb03f52f..0afe9e070 100644 --- a/examples/resources/stackit_logme_instance/import-by-string-id.tf +++ b/examples/resources/stackit_logme_instance/import-by-string-id.tf @@ -1,5 +1,5 @@ # Only use the import statement, if you want to import an existing logme instance import { to = stackit_logme_instance.import-example - id = "${var.project_id},${var.logme_instance_id}" + id = "${var.project_id},${var.region},${var.logme_instance_id}" } diff --git a/go.mod b/go.mod index f530b0b66..0a2ceedf1 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/intake v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/kms v1.12.0 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/logme v1.1.0 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/modelserving v0.11.0 diff --git a/go.sum b/go.sum index 78bb4a31f..7001540db 100644 --- a/go.sum +++ b/go.sum @@ -696,8 +696,8 @@ github.com/stackitcloud/stackit-sdk-go/services/kms v1.12.0 h1:rydjdSL+j6Yd+3T7k github.com/stackitcloud/stackit-sdk-go/services/kms v1.12.0/go.mod h1:pVaCmb1ZHAPGVRlSlBlVOjThp9Tb2sX9+nRX0M+d1KU= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.15.0 h1:YWpy3VDKrTKVgvnHgbLFYRZ4qUKiNxzCejUNdWIYZ6Q= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.15.0/go.mod h1:+Ld3dn648I+YKcBV3fEkYpDSr3fel421+LurJGywSBs= -github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1 h1:iteL61eMMPWT6872nF4Ko/tBU1IXemvS++XR09pj6NA= -github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1/go.mod h1:JDOOYaGgcBts2x52nKPRMFgSZe7qqOFmfz1xIXCQgRY= +github.com/stackitcloud/stackit-sdk-go/services/logme v1.1.0 h1:r+MzTC/qI8t4zTB029bp75iLzsx1/F4LzTiZeIwM5Vg= +github.com/stackitcloud/stackit-sdk-go/services/logme v1.1.0/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= diff --git a/stackit/internal/services/logme/credential/datasource.go b/stackit/internal/services/logme/credential/datasource.go index 3b1eb03e9..519d67321 100644 --- a/stackit/internal/services/logme/credential/datasource.go +++ b/stackit/internal/services/logme/credential/datasource.go @@ -17,7 +17,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" ) // Ensure the implementation satisfies the expected interfaces. @@ -32,37 +32,40 @@ func NewCredentialDataSource() datasource.DataSource { // credentialDataSource is the data source implementation. type credentialDataSource struct { - client *logmeSdk.APIClient + client *logmeSdk.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 + "_logme_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 := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := logmeUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - r.client = apiClient + d.client = apiClient tflog.Info(ctx, "LogMe 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": "LogMe 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 LogMe 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{ @@ -113,12 +116,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 Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -129,13 +138,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, @@ -154,7 +165,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(recordSetResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return diff --git a/stackit/internal/services/logme/credential/resource.go b/stackit/internal/services/logme/credential/resource.go index d1d90f79a..298398bbd 100644 --- a/stackit/internal/services/logme/credential/resource.go +++ b/stackit/internal/services/logme/credential/resource.go @@ -25,8 +25,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/logme/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 { @@ -46,6 +47,7 @@ type Model struct { Port types.Int32 `tfsdk:"port"` Uri types.String `tfsdk:"uri"` Username types.String `tfsdk:"username"` + Region types.String `tfsdk:"region"` } // NewCredentialResource is a helper function to simplify the provider implementation. @@ -55,7 +57,8 @@ func NewCredentialResource() resource.Resource { // credentialResource is the resource implementation. type credentialResource struct { - client *logmeSdk.APIClient + client *logmeSdk.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -65,12 +68,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 := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := logmeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -78,14 +82,45 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu tflog.Info(ctx, "LogMe 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": "LogMe 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 LogMe 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{ @@ -150,6 +185,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, "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"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, }, } } @@ -166,12 +210,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 @@ -183,6 +229,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, }) @@ -190,14 +237,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(waitResp, &model) + err = mapFields(waitResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -222,6 +269,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 == "" { @@ -230,10 +278,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 { @@ -247,7 +296,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(recordSetResp, &model) + err = mapFields(recordSetResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) return @@ -280,14 +329,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 { @@ -299,7 +350,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 @@ -308,26 +359,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, "LogMe credential state imported") } -func mapFields(credentialsResp *logmeSdk.CredentialsResponse, model *Model) error { +func mapFields(credentialsResp *logmeSdk.CredentialsResponse, model *Model, region string) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } @@ -348,7 +400,8 @@ func mapFields(credentialsResp *logmeSdk.CredentialsResponse, model *Model) erro return fmt.Errorf("credentials id not present") } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId) + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), credentialId) + model.Region = types.StringValue(region) model.CredentialId = types.StringValue(credentialId) model.Host = types.StringValue(credentials.Host) model.Password = types.StringValue(credentials.Password) diff --git a/stackit/internal/services/logme/credential/resource_test.go b/stackit/internal/services/logme/credential/resource_test.go index b8d6048d8..e40bb5090 100644 --- a/stackit/internal/services/logme/credential/resource_test.go +++ b/stackit/internal/services/logme/credential/resource_test.go @@ -1,13 +1,16 @@ package logme import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/types" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" ) +const testRegion = "eu02" + func TestMapFields(t *testing.T) { tests := []struct { description string @@ -22,7 +25,7 @@ func TestMapFields(t *testing.T) { Raw: &logmeSdk.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"), @@ -31,6 +34,7 @@ func TestMapFields(t *testing.T) { Port: types.Int32Null(), Uri: types.StringNull(), Username: types.StringValue(""), + Region: types.StringValue(testRegion), }, true, }, @@ -49,7 +53,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"), @@ -58,6 +62,7 @@ func TestMapFields(t *testing.T) { Port: types.Int32Value(1234), Uri: types.StringValue("uri"), Username: types.StringValue("username"), + Region: types.StringValue(testRegion), }, true, }, @@ -76,7 +81,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 +90,7 @@ func TestMapFields(t *testing.T) { Port: types.Int32Value(2123456789), Uri: types.StringNull(), Username: types.StringValue(""), + Region: types.StringValue(testRegion), }, true, }, @@ -115,7 +121,7 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } - err := mapFields(tt.input, model) + err := mapFields(tt.input, model, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/logme/instance/datasource.go b/stackit/internal/services/logme/instance/datasource.go index 47add8812..0e14166fb 100644 --- a/stackit/internal/services/logme/instance/datasource.go +++ b/stackit/internal/services/logme/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" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/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 *logmeSdk.APIClient + client *logmeSdk.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 + "_logme_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 := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := logmeUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } - r.client = apiClient + d.client = apiClient tflog.Info(ctx, "LogMe 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": "LogMe 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 LogMe 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{ @@ -235,12 +238,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...) @@ -251,11 +260,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, @@ -274,14 +285,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/logme/instance/resource.go b/stackit/internal/services/logme/instance/resource.go index 292a7b930..cef9a517b 100644 --- a/stackit/internal/services/logme/instance/resource.go +++ b/stackit/internal/services/logme/instance/resource.go @@ -30,9 +30,9 @@ 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" - "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" + "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api/wait" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" ) // Ensure the implementation satisfies the expected interfaces. @@ -40,6 +40,7 @@ var ( _ resource.Resource = &instanceResource{} _ resource.ResourceWithConfigure = &instanceResource{} _ resource.ResourceWithImportState = &instanceResource{} + _ resource.ResourceWithModifyPlan = &instanceResource{} ) type Model struct { @@ -56,6 +57,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 @@ -117,7 +119,8 @@ func NewInstanceResource() resource.Resource { // instanceResource is the resource implementation. type instanceResource struct { - client *logmeSdk.APIClient + client *logmeSdk.APIClient + providerData core.ProviderData } // Metadata returns the resource type name. @@ -127,12 +130,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 := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := logmeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -140,11 +144,41 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure tflog.Info(ctx, "LogMe 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": "LogMe 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 LogMe instance.", "project_id": "STACKIT project ID to which the instance is associated.", "name": "Instance name.", @@ -152,6 +186,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{ @@ -380,6 +415,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(), + }, + }, }, } } @@ -396,7 +440,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()) { @@ -408,7 +454,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 @@ -421,7 +467,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 @@ -433,20 +479,21 @@ 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() { return } - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, instanceId).SetTimeout(90 * time.Minute).WaitWithContext(ctx) + waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client.DefaultAPI, projectId, region, instanceId).SetTimeout(90 * time.Minute).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 @@ -473,6 +520,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. @@ -480,9 +528,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) { @@ -496,14 +545,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 @@ -530,8 +579,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 @@ -544,7 +595,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 @@ -557,7 +608,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 @@ -565,14 +616,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 @@ -599,12 +650,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 { @@ -617,7 +670,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 @@ -626,26 +679,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, "LogMe instance state imported") } -func mapFields(instance *logmeSdk.Instance, model *Model) error { +func mapFields(instance *logmeSdk.Instance, model *Model, region string) error { if instance == nil { return fmt.Errorf("response input is nil") } @@ -662,7 +716,8 @@ func mapFields(instance *logmeSdk.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) @@ -912,9 +967,9 @@ func toInstanceParams(parameters *parametersModel) (*logmeSdk.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 LogMe offerings: %w", err) } @@ -949,10 +1004,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 *logmeSdk.APIClient, model *Model) error { +func loadPlanNameAndVersion(ctx context.Context, client *logmeSdk.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 LogMe offerings: %w", err) } diff --git a/stackit/internal/services/logme/instance/resource_test.go b/stackit/internal/services/logme/instance/resource_test.go index 42ec63eb0..a03ca853c 100644 --- a/stackit/internal/services/logme/instance/resource_test.go +++ b/stackit/internal/services/logme/instance/resource_test.go @@ -2,15 +2,18 @@ package logme 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" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" ) +const testRegion = "eu02" + var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ "sgw_acl": types.StringValue("acl"), "enable_monitoring": types.BoolValue(true), @@ -106,7 +109,7 @@ func TestMapFields(t *testing.T) { "default_values", &logmeSdk.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(""), @@ -117,6 +120,7 @@ func TestMapFields(t *testing.T) { ImageUrl: types.StringValue(""), CfOrganizationGuid: types.StringValue(""), Parameters: types.ObjectNull(parametersTypes), + Region: types.StringValue(testRegion), }, true, }, @@ -159,7 +163,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"), @@ -170,6 +174,7 @@ func TestMapFields(t *testing.T) { ImageUrl: types.StringValue("image"), CfOrganizationGuid: types.StringValue("org"), Parameters: fixtureModelParameters, + Region: types.StringValue(testRegion), }, true, }, @@ -212,7 +217,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/logme/logme_acc_test.go b/stackit/internal/services/logme/logme_acc_test.go index 0a3869a89..aa69234de 100644 --- a/stackit/internal/services/logme/logme_acc_test.go +++ b/stackit/internal/services/logme/logme_acc_test.go @@ -17,8 +17,8 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/logme/v2api/wait" ) var ( @@ -164,11 +164,15 @@ func TestAccLogMeMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_logme_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, @@ -182,6 +186,10 @@ func TestAccLogMeMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_logme_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") @@ -190,7 +198,7 @@ func TestAccLogMeMinResource(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, @@ -329,11 +337,15 @@ func TestAccLogMeMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_logme_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, @@ -347,6 +359,10 @@ func TestAccLogMeMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource stackit_logme_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") @@ -355,7 +371,7 @@ func TestAccLogMeMaxResource(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, @@ -401,7 +417,7 @@ func TestAccLogMeMaxResource(t *testing.T) { func testAccCheckLogMeDestroy(s *terraform.State) error { ctx := context.Background() - client, err := logmeSdk.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.LogMeCustomEndpoint, true)...) + client, err := logmeSdk.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.LogMeCustomEndpoint, false)...) if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -411,12 +427,12 @@ func testAccCheckLogMeDestroy(s *terraform.State) error { if rs.Type != "stackit_logme_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) } @@ -428,11 +444,11 @@ func testAccCheckLogMeDestroy(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/logme/utils/util.go b/stackit/internal/services/logme/utils/util.go index 8a4209a63..c4b34b4f6 100644 --- a/stackit/internal/services/logme/utils/util.go +++ b/stackit/internal/services/logme/utils/util.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/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.LogMeCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.LogMeCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := logmeSdk.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/logme/utils/util_test.go b/stackit/internal/services/logme/utils/util_test.go index 36aa9c8cb..9bb38397a 100644 --- a/stackit/internal/services/logme/utils/util_test.go +++ b/stackit/internal/services/logme/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" - logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + logmeSdk "github.com/stackitcloud/stackit-sdk-go/services/logme/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() *logmeSdk.APIClient { apiClient, err := logmeSdk.NewAPIClient( - config.WithRegion("eu01"), utils.UserAgentConfigOption(testVersion), ) if err != nil {