diff --git a/docs/data-sources/logme_instance.md b/docs/data-sources/logme_instance.md index 8392835d..f232f61b 100644 --- a/docs/data-sources/logme_instance.md +++ b/docs/data-sources/logme_instance.md @@ -33,7 +33,7 @@ data "stackit_logme_instance" "example" { - `cf_organization_guid` (String) - `cf_space_guid` (String) - `dashboard_url` (String) -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`zone_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`instance_id`". - `image_url` (String) - `name` (String) Instance name. - `parameters` (Attributes) (see [below for nested schema](#nestedatt--parameters)) diff --git a/docs/data-sources/mariadb_credentials.md b/docs/data-sources/mariadb_credentials.md index f948838b..dff23677 100644 --- a/docs/data-sources/mariadb_credentials.md +++ b/docs/data-sources/mariadb_credentials.md @@ -34,7 +34,7 @@ data "stackit_mariadb_credentials" "example" { - `host` (String) - `hosts` (List of String) - `http_api_uri` (String) -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`,`credentials_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`instance_id`,`credentials_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 e6150d9d..e3af58c2 100644 --- a/docs/data-sources/mariadb_instance.md +++ b/docs/data-sources/mariadb_instance.md @@ -33,7 +33,7 @@ data "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 identifier. It is structured as "`project_id`,`instance_id`". - `image_url` (String) - `name` (String) Instance name. - `parameters` (Attributes) (see [below for nested schema](#nestedatt--parameters)) diff --git a/docs/resources/mariadb_credentials.md b/docs/resources/mariadb_credentials.md index 6eb34801..22c495d1 100644 --- a/docs/resources/mariadb_credentials.md +++ b/docs/resources/mariadb_credentials.md @@ -33,7 +33,7 @@ resource "stackit_mariadb_credentials" "example" { - `host` (String) - `hosts` (List of String) - `http_api_uri` (String) -- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`,`credentials_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`instance_id`,`credentials_id`". - `name` (String) - `password` (String, Sensitive) - `port` (Number) diff --git a/stackit/core/core.go b/stackit/core/core.go index 1c1dfcb3..74619d12 100644 --- a/stackit/core/core.go +++ b/stackit/core/core.go @@ -52,5 +52,5 @@ func DiagsToError(diags diag.Diagnostics) error { // LogAndAddError Logs the error and adds it to the diags func LogAndAddError(ctx context.Context, diags *diag.Diagnostics, summary, detail string) { tflog.Error(ctx, summary) - (*diags).AddError(summary, detail) + diags.AddError(summary, detail) } diff --git a/stackit/provider.go b/stackit/provider.go index 78c4fc8e..d34e016f 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -2,6 +2,7 @@ package stackit import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -225,10 +226,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, } roundTripper, err := sdkauth.SetupAuth(sdkConfig) if err != nil { - resp.Diagnostics.AddError( - "Unable to Setup SDK", - err.Error(), - ) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up authentication: %v", err)) return } @@ -274,7 +272,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { postgresInstance.NewInstanceResource, postgresCredentials.NewCredentialsResource, logMeInstance.NewInstanceResource, - logMeCredentials.NewlogmeCredentialsResource, + logMeCredentials.NewCredentialsResource, mariaDBInstance.NewInstanceResource, mariaDBCredentials.NewCredentialsResource, openSearchInstance.NewInstanceResource, diff --git a/stackit/services/argus/credential/resource.go b/stackit/services/argus/credential/resource.go index d8efb4a3..44befbb0 100644 --- a/stackit/services/argus/credential/resource.go +++ b/stackit/services/argus/credential/resource.go @@ -49,7 +49,7 @@ func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRe } // Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -57,7 +57,7 @@ func (r *credentialResource) Configure(_ context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -76,10 +76,12 @@ func (r *credentialResource) Configure(_ context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", err.Error()) return } + r.client = apiClient + tflog.Info(ctx, "Argus credential client configured") } func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -148,17 +150,20 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ got, err := r.client.CreateCredential(ctx, instanceId, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error creating credential", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } err = mapFields(got.Credentials, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "ARGUS credential created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus credential created") } func mapFields(r *argus.Credential, model *Model) error { @@ -202,17 +207,20 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, userName := model.Username.ValueString() _, err := r.client.GetCredential(ctx, instanceId, projectId, userName).Execute() if err != nil { - resp.Diagnostics.AddError("Error reading credential", fmt.Sprintf("Project id = %s, instance id = %s, username = %s: %v", projectId, instanceId, userName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "ARGUS credential read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus credential read") } -func (r *credentialResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -229,8 +237,8 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ userName := model.Username.ValueString() _, err := r.client.DeleteCredential(ctx, instanceId, projectId, userName).Execute() if err != nil { - resp.Diagnostics.AddError("Error deleting credential", "project id = "+projectId+", instance id = "+instanceId+", username = "+userName+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) return } - tflog.Info(ctx, "ARGUS credential deleted") + tflog.Info(ctx, "Argus credential deleted") } diff --git a/stackit/services/argus/instance/datasource.go b/stackit/services/argus/instance/datasource.go index 5bf173bf..fdd89085 100644 --- a/stackit/services/argus/instance/datasource.go +++ b/stackit/services/argus/instance/datasource.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/argus" "github.com/stackitcloud/terraform-provider-stackit/stackit/core" @@ -35,7 +36,7 @@ func (d *instanceDataSource) Metadata(_ context.Context, req datasource.Metadata resp.TypeName = req.ProviderTypeName + "_argus_instance" } -func (d *instanceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -46,7 +47,7 @@ func (d *instanceDataSource) Configure(_ context.Context, req datasource.Configu providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -62,13 +63,11 @@ func (d *instanceDataSource) Configure(_ context.Context, req datasource.Configu ) } if err != nil { - resp.Diagnostics.AddError( - "Could not Configure API Client", - err.Error(), - ) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } d.client = apiClient + tflog.Info(ctx, "Argus instance client configured") } // Schema defines the schema for the data source. @@ -202,28 +201,29 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() instanceResponse, err := d.client.GetInstance(ctx, instanceId, projectId).Execute() if err != nil { - core.LogAndAddError(ctx, &diags, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(ctx, instanceResponse, &state) + err = mapFields(ctx, instanceResponse, &model) if err != nil { - core.LogAndAddError(ctx, &diags, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Argus instance read") } diff --git a/stackit/services/argus/instance/resource.go b/stackit/services/argus/instance/resource.go index fd1a3734..dff94f44 100644 --- a/stackit/services/argus/instance/resource.go +++ b/stackit/services/argus/instance/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -17,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/argus" "github.com/stackitcloud/terraform-provider-stackit/stackit/core" @@ -75,7 +75,7 @@ func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequ } // Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -83,7 +83,7 @@ func (r *instanceResource) Configure(_ context.Context, req resource.ConfigureRe providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -102,10 +102,12 @@ func (r *instanceResource) Configure(_ context.Context, req resource.ConfigureRe } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } + r.client = apiClient + tflog.Info(ctx, "Argus instance client configured") } // Schema defines the schema for the resource. @@ -264,48 +266,50 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } projectId := model.ProjectId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if diags.HasError() { - core.LogAndAddError(ctx, &diags, "Failed to load argus service plan", "plan "+model.PlanName.ValueString()) + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) return } // Generate API request body from model payload, err := toCreatePayload(&model) if err != nil { - resp.Diagnostics.AddError("Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) return } createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() if err != nil { - resp.Diagnostics.AddError("Error creating instance", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } instanceId := createResp.InstanceId - if instanceId == nil || *instanceId == "" { - resp.Diagnostics.AddError("Error creating instance", "API didn't return an instance id") - return - } + ctx = tflog.SetField(ctx, "instance_id", instanceId) wr, err := argus.CreateInstanceWaitHandler(ctx, r.client, *instanceId, projectId).SetTimeout(20 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) return } got, ok := wr.(*argus.InstanceResponse) if !ok { - resp.Diagnostics.AddError("Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(ctx, got, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields", fmt.Sprintf("Project id %s, instance id %s: %v", projectId, *instanceId, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus instance created") } // Read refreshes the Terraform state with the latest data. @@ -318,22 +322,28 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, instanceId, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error reading instance", fmt.Sprintf("Project id = %s, instance id = %s: %v", projectId, instanceId, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(ctx, instanceResp, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields", fmt.Sprintf("Project id %s, instance id %s: %v", projectId, instanceId, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus instance created") } // Update updates the resource and sets the updated Terraform state on success. @@ -348,42 +358,46 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if diags.HasError() { - core.LogAndAddError(ctx, &diags, "Failed to load argus service plan", "plan "+model.PlanName.ValueString()) + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) return } // Generate API request body from model payload, err := toUpdatePayload(&model) if err != nil { - resp.Diagnostics.AddError("Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance _, err = r.client.UpdateInstance(ctx, instanceId, projectId).UpdateInstancePayload(*payload).Execute() if err != nil { - resp.Diagnostics.AddError("Error updating instance", "project id = "+projectId+", instance Id = "+instanceId+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := argus.UpdateInstanceWaitHandler(ctx, r.client, instanceId, projectId).SetTimeout(20 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) return } got, ok := wr.(*argus.InstanceResponse) if !ok { - resp.Diagnostics.AddError("Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } err = mapFields(ctx, got, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields in update", "project id = "+projectId+", instance Id = "+instanceId+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus instance updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -402,14 +416,16 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance _, err := r.client.DeleteInstance(ctx, instanceId, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error deleting instance", "project id = "+projectId+", instance Id = "+instanceId+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = argus.DeleteInstanceWaitHandler(ctx, r.client, instanceId, projectId).SetTimeout(10 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + + tflog.Info(ctx, "Argus instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -418,8 +434,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -427,6 +443,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) + tflog.Info(ctx, "Argus instance state imported") } func mapFields(ctx context.Context, r *argus.InstanceResponse, model *Model) error { @@ -529,12 +546,11 @@ func toUpdatePayload(model *Model) (*argus.UpdateInstancePayload, error) { }, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetPlans(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list argus plans", err.Error()) - return + return err } planName := model.PlanName.ValueString() @@ -552,7 +568,7 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti avl = fmt.Sprintf("%s\n- %s", avl, *p.Name) } if model.PlanId.ValueString() == "" { - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s', available names are:%s", planName, avl)) - return + return fmt.Errorf("couldn't find plan_name '%s', available names are: %s", planName, avl) } + return nil } diff --git a/stackit/services/argus/scrapeconfig/datasource.go b/stackit/services/argus/scrapeconfig/datasource.go index e6843d21..617e2869 100644 --- a/stackit/services/argus/scrapeconfig/datasource.go +++ b/stackit/services/argus/scrapeconfig/datasource.go @@ -37,7 +37,7 @@ func (d *scrapeConfigDataSource) Metadata(_ context.Context, req datasource.Meta resp.TypeName = req.ProviderTypeName + "_argus_scrapeconfig" } -func (d *scrapeConfigDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +func (d *scrapeConfigDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -48,7 +48,7 @@ func (d *scrapeConfigDataSource) Configure(_ context.Context, req datasource.Con providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,10 +64,7 @@ func (d *scrapeConfigDataSource) Configure(_ context.Context, req datasource.Con ) } if err != nil { - resp.Diagnostics.AddError( - "Could not Configure API Client", - err.Error(), - ) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } d.client = apiClient @@ -204,13 +201,13 @@ func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRe scResp, err := d.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { - core.LogAndAddError(ctx, &diags, "Unable to read scrape config", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read scrape config", err.Error()) return } err = mapFields(scResp.Data, &model) if err != nil { - core.LogAndAddError(ctx, &diags, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) return } diags = resp.State.Set(ctx, model) diff --git a/stackit/services/argus/scrapeconfig/resource.go b/stackit/services/argus/scrapeconfig/resource.go index 1be32636..10adb7e7 100644 --- a/stackit/services/argus/scrapeconfig/resource.go +++ b/stackit/services/argus/scrapeconfig/resource.go @@ -86,7 +86,7 @@ func (r *scrapeConfigResource) Metadata(_ context.Context, req resource.Metadata } // Configure adds the provider configured client to the resource. -func (r *scrapeConfigResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *scrapeConfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -94,7 +94,7 @@ func (r *scrapeConfigResource) Configure(_ context.Context, req resource.Configu providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -113,10 +113,11 @@ func (r *scrapeConfigResource) Configure(_ context.Context, req resource.Configu } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", err.Error()) return } r.client = apiClient + tflog.Info(ctx, "Argus scrape config client configured") } // Schema defines the schema for the resource. @@ -277,33 +278,36 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe // Generate API request body from model payload, err := toCreatePayload(ctx, &model) if err != nil { - resp.Diagnostics.AddError("Error creating scrape config", fmt.Sprintf("Creating API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Creating API payload: %v", err)) return } _, err = r.client.CreateScrapeConfig(ctx, instanceId, projectId).CreateScrapeConfigPayload(*payload).Execute() if err != nil { - resp.Diagnostics.AddError("Error creating scrape config", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API: %v", err)) return } _, err = argus.CreateScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).SetTimeout(3 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error creating scrape config", fmt.Sprintf("ScrapeConfig creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Scrape config creation waiting: %v", err)) return } got, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error creating scrape config", fmt.Sprintf("ScrapeConfig creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) return } err = mapFields(got.Data, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields", fmt.Sprintf("Project id %s, ScrapeConfig id %s: %v", projectId, scName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "ARGUS scrape config created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus scrape config created") } // Read refreshes the Terraform state with the latest data. @@ -320,20 +324,23 @@ func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadReques scResp, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error reading scrape config", fmt.Sprintf("Project id = %s, instance id = %s, scrape config name = %s: %v", projectId, instanceId, scName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading scrape config", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(scResp.Data, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields", fmt.Sprintf("Project id = %s, instance id = %s, sc name = %s: %v", projectId, instanceId, scName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading scrape config", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "ARGUS scrape config read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus scrape config read") } // Update updates the resource and sets the updated Terraform state on success. @@ -352,12 +359,12 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe // Generate API request body from model payload, err := toUpdatePayload(ctx, &model) if err != nil { - resp.Diagnostics.AddError("Error updating scrape config", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Creating API payload: %v", err)) return } _, err = r.client.UpdateScrapeConfig(ctx, instanceId, scName, projectId).UpdateScrapeConfigPayload(*payload).Execute() if err != nil { - resp.Diagnostics.AddError("Error updating scrape config", fmt.Sprintf("Project id = %s, instance id = %s, scrape config name = %s: %v", projectId, instanceId, scName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API: %v", err)) return } // We do not have an update status provided by the argus scrape config api, so we cannot use a waiter here, hence a simple sleep is used. @@ -366,17 +373,20 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe // Fetch updated ScrapeConfig scResp, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error reading updated data", fmt.Sprintf("Project id %s, instance id %s, jo name %s: %v", projectId, instanceId, scName, err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) return } err = mapFields(scResp.Data, &model) if err != nil { - resp.Diagnostics.AddError("Error mapping fields in update", "project id = "+projectId+", instance id = "+instanceId+", scrape config name = "+scName+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "ARGUS scrape config updated") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Argus scrape config updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -396,15 +406,16 @@ func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRe // Delete existing ScrapeConfig _, err := r.client.DeleteScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("Error deleting scrape config", "project id = "+projectId+", instance id = "+instanceId+", scrape config name = "+scName+", "+err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Calling API: %v", err)) return } _, err = argus.DeleteScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error deleting scrape config", fmt.Sprintf("ScrapeConfig deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Scrape config deletion waiting: %v", err)) return } - tflog.Info(ctx, "ARGUS scrape config deleted") + + tflog.Info(ctx, "Argus scrape config deleted") } // ImportState imports a resource into the Terraform state on success. @@ -413,8 +424,8 @@ func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.Imp idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing scrape config", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), ) return @@ -423,7 +434,7 @@ func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.Imp resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "ARGUS scrape config state imported") + tflog.Info(ctx, "Argus scrape config state imported") } func mapFields(sc *argus.Job, model *Model) error { diff --git a/stackit/services/dns/recordset/datasource.go b/stackit/services/dns/recordset/datasource.go index 681cb26e..517f3cd8 100644 --- a/stackit/services/dns/recordset/datasource.go +++ b/stackit/services/dns/recordset/datasource.go @@ -44,7 +44,7 @@ func (d *recordSetDataSource) Configure(ctx context.Context, req datasource.Conf providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -62,12 +62,12 @@ func (d *recordSetDataSource) Configure(ctx context.Context, req datasource.Conf } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "DNS record set client configured") d.client = apiClient + tflog.Info(ctx, "DNS record set client configured") } // Schema defines the schema for the data source. @@ -142,33 +142,33 @@ func (d *recordSetDataSource) Schema(_ context.Context, _ datasource.SchemaReque // Read refreshes the Terraform state with the latest data. func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - zoneId := state.ZoneId.ValueString() - recordSetId := state.RecordSetId.ValueString() + projectId := model.ProjectId.ValueString() + zoneId := model.ZoneId.ValueString() + recordSetId := model.RecordSetId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "zone_id", zoneId) ctx = tflog.SetField(ctx, "record_set_id", recordSetId) zoneResp, err := d.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to Read record set", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(zoneResp, &state) + err = mapFields(zoneResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) return } - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "DNS record set created") + tflog.Info(ctx, "DNS record set read") } diff --git a/stackit/services/dns/recordset/resource.go b/stackit/services/dns/recordset/resource.go index 81e114b0..36a4a7f3 100644 --- a/stackit/services/dns/recordset/resource.go +++ b/stackit/services/dns/recordset/resource.go @@ -72,7 +72,7 @@ func (r *recordSetResource) Configure(ctx context.Context, req resource.Configur providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -90,12 +90,12 @@ func (r *recordSetResource) Configure(ctx context.Context, req resource.Configur } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Debug(ctx, "DNS record set client configured") r.client = apiClient + tflog.Info(ctx, "DNS record set client configured") } // Schema defines the schema for the resource. @@ -225,37 +225,40 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque // Generate API request body from model payload, err := toCreatePayload(&model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating recordset", fmt.Sprintf("Creating API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Creating API payload: %v", err)) return } // Create new recordset recordSetResp, err := r.client.CreateRecordSet(ctx, projectId, zoneId).CreateRecordSetPayload(*payload).Execute() if err != nil || recordSetResp.Rrset == nil || recordSetResp.Rrset.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating recordset", fmt.Sprintf("Calling API: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Calling API: %v", err)) return } ctx = tflog.SetField(ctx, "record_set_id", *recordSetResp.Rrset.Id) wr, err := dns.CreateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, *recordSetResp.Rrset.Id).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating recordset", fmt.Sprintf("Instance creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Instance creation waiting: %v", err)) return } got, ok := wr.(*dns.RecordSetResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating recordset", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS record set created") } @@ -276,20 +279,23 @@ func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, recordSetResp, err := r.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zones", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS record set read") } @@ -313,39 +319,42 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque // Generate API request body from model payload, err := toUpdatePayload(&model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating recordset", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Creating API payload: %v", err)) return } // Update recordset _, err = r.client.UpdateRecordSet(ctx, projectId, zoneId, recordSetId).UpdateRecordSetPayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating recordset", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", err.Error()) return } wr, err := dns.UpdateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating recordset", fmt.Sprintf("Instance update waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Instance update waiting: %v", err)) return } - got, ok := wr.(*dns.RecordSetResponse) + _, ok := wr.(*dns.RecordSetResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating recordset", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } // Fetch updated record set recordSetResp, err := r.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading updated data", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Calling API for updated data: %v", err)) return } err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS record set updated") } @@ -369,7 +378,7 @@ func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteReque // Delete existing record set _, err := r.client.DeleteRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting recordset", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Calling API: %v", err)) } _, err = dns.DeleteRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -384,8 +393,8 @@ func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteReque func (r *recordSetResource) 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] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing record set", fmt.Sprintf("Expected import identifier with format [project_id],[zone_id],[record_set_id], got %q", req.ID), ) return diff --git a/stackit/services/dns/zone/datasource.go b/stackit/services/dns/zone/datasource.go index 9b41f01f..6ad7d148 100644 --- a/stackit/services/dns/zone/datasource.go +++ b/stackit/services/dns/zone/datasource.go @@ -46,7 +46,7 @@ func (d *zoneDataSource) Configure(ctx context.Context, req datasource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -61,15 +61,12 @@ func (d *zoneDataSource) Configure(ctx context.Context, req datasource.Configure ) } if err != nil { - resp.Diagnostics.AddError( - "Could not Configure API Client", - err.Error(), - ) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "DNS zone client configured") d.client = apiClient + tflog.Info(ctx, "DNS zone client configured") } // Schema defines the schema for the data source. @@ -180,29 +177,29 @@ func (d *zoneDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r // Read refreshes the Terraform state with the latest data. func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - zoneId := state.ZoneId.ValueString() + projectId := model.ProjectId.ValueString() + zoneId := model.ZoneId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "zone_id", zoneId) zoneResp, err := d.client.GetZone(ctx, projectId, zoneId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to Read Zone", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(zoneResp, &state) + err = mapFields(zoneResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) return } - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/stackit/services/dns/zone/resource.go b/stackit/services/dns/zone/resource.go index 7b63af93..e7740bd7 100644 --- a/stackit/services/dns/zone/resource.go +++ b/stackit/services/dns/zone/resource.go @@ -85,7 +85,7 @@ func (r *zoneResource) Configure(ctx context.Context, req resource.ConfigureRequ providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -104,12 +104,12 @@ func (r *zoneResource) Configure(ctx context.Context, req resource.ConfigureRequ } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "DNS zone client configured") r.client = apiClient + tflog.Info(ctx, "DNS zone client configured") } // Schema defines the schema for the resource. @@ -318,64 +318,66 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Calling API: %v", err)) return } - if createResp.Zone.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", "API didn't return zone id") - return - } zoneId := *createResp.Zone.Id ctx = tflog.SetField(ctx, "zone_id", zoneId) wr, err := dns.CreateZoneWaitHandler(ctx, r.client, projectId, zoneId).SetTimeout(10 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Instance creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Zone creation waiting: %v", err)) return } got, ok := wr.(*dns.ZoneResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS zone created") } // Read refreshes the Terraform state with the latest data. func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - zoneId := state.ZoneId.ValueString() + projectId := model.ProjectId.ValueString() + zoneId := model.ZoneId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "zone_id", zoneId) zoneResp, err := r.client.GetZone(ctx, projectId, zoneId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zones", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(zoneResp, &state) + // Map response body to schema + err = mapFields(zoneResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS zone read") } @@ -396,39 +398,42 @@ func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, r // Generate API request body from model payload, err := toUpdatePayload(&model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing zone _, err = r.client.UpdateZone(ctx, projectId, zoneId).UpdateZonePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Calling API: %v", err)) return } wr, err := dns.UpdateZoneWaitHandler(ctx, r.client, projectId, zoneId).SetTimeout(10 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Instance update waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Zone update waiting: %v", err)) return } - got, ok := wr.(*dns.ZoneResponse) + _, ok := wr.(*dns.ZoneResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } // Fetch updated zone zoneResp, err := r.client.GetZone(ctx, projectId, zoneId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading updated data", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Calling API for updated data: %v", err)) return } err = mapFields(zoneResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "DNS zone updated") } @@ -450,12 +455,12 @@ func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, r // Delete existing zone _, err := r.client.DeleteZone(ctx, projectId, zoneId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Calling API: %v", err)) return } _, err = dns.DeleteZoneWaitHandler(ctx, r.client, projectId, zoneId).SetTimeout(10 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Instance deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Zone deletion waiting: %v", err)) return } @@ -468,8 +473,8 @@ func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportState idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing zone", fmt.Sprintf("Expected import identifier with format: [project_id],[zone_id] Got: %q", req.ID), ) return diff --git a/stackit/services/logme/credentials/datasource.go b/stackit/services/logme/credentials/datasource.go index 0bd1bb18..d2d951e4 100644 --- a/stackit/services/logme/credentials/datasource.go +++ b/stackit/services/logme/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "LogMe zone client configured") r.client = apiClient + tflog.Info(ctx, "LogMe credentials client configured") } // Schema defines the schema for the resource. @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "LogMe credentials read") } diff --git a/stackit/services/logme/credentials/resource.go b/stackit/services/logme/credentials/resource.go index db62619e..f1e32f15 100644 --- a/stackit/services/logme/credentials/resource.go +++ b/stackit/services/logme/credentials/resource.go @@ -25,9 +25,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &logmeCredentialsResource{} - _ resource.ResourceWithConfigure = &logmeCredentialsResource{} - _ resource.ResourceWithImportState = &logmeCredentialsResource{} + _ resource.Resource = &credentialsResource{} + _ resource.ResourceWithConfigure = &credentialsResource{} + _ resource.ResourceWithImportState = &credentialsResource{} ) type Model struct { @@ -45,23 +45,23 @@ type Model struct { Username types.String `tfsdk:"username"` } -// NewlogmeCredentialsResource is a helper function to simplify the provider implementation. -func NewlogmeCredentialsResource() resource.Resource { - return &logmeCredentialsResource{} +// NewCredentialsResource is a helper function to simplify the provider implementation. +func NewCredentialsResource() resource.Resource { + return &credentialsResource{} } // credentialsResource is the resource implementation. -type logmeCredentialsResource struct { +type credentialsResource struct { client *logme.APIClient } // Metadata returns the resource type name. -func (r *logmeCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *credentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_logme_credentials" } // Configure adds the provider configured client to the resource. -func (r *logmeCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *credentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -69,7 +69,7 @@ func (r *logmeCredentialsResource) Configure(ctx context.Context, req resource.C providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,16 +88,16 @@ func (r *logmeCredentialsResource) Configure(ctx context.Context, req resource.C } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "logme zone client configured") r.client = apiClient + tflog.Info(ctx, "LogMe credentials client configured") } // Schema defines the schema for the resource. -func (r *logmeCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *credentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "LogMe credentials resource schema.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", @@ -182,7 +182,7 @@ func (r *logmeCredentialsResource) Schema(_ context.Context, _ resource.SchemaRe } // Create creates the resource and sets the initial Terraform state. -func (r *logmeCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -214,23 +214,26 @@ func (r *logmeCredentialsResource) Create(ctx context.Context, req resource.Crea } got, ok := wr.(*logme.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "LogMe credentials created") } // Read refreshes the Terraform state with the latest data. -func (r *logmeCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -246,31 +249,34 @@ func (r *logmeCredentialsResource) Read(ctx context.Context, req resource.ReadRe recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "LogMe credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *logmeCredentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *logmeCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -288,7 +294,7 @@ func (r *logmeCredentialsResource) Delete(ctx context.Context, req resource.Dele // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = logme.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -300,11 +306,11 @@ func (r *logmeCredentialsResource) Delete(ctx context.Context, req resource.Dele // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,instance_id,credentials_id -func (r *logmeCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *credentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return diff --git a/stackit/services/logme/instance/datasource.go b/stackit/services/logme/instance/datasource.go index 8f73c0a6..8b360f07 100644 --- a/stackit/services/logme/instance/datasource.go +++ b/stackit/services/logme/instance/datasource.go @@ -44,7 +44,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,19 +63,19 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "LogMe zone client configured") r.client = apiClient + tflog.Info(ctx, "LogMe zone client configured") } // Schema defines the schema for the resource. func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ "main": "LogMe instance data source schema.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`zone_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`\".", "instance_id": "ID of the LogMe instance.", "project_id": "STACKIT Project ID to which the instance is associated.", "name": "Instance name.", @@ -152,37 +152,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "LogMe instance read") } diff --git a/stackit/services/logme/instance/resource.go b/stackit/services/logme/instance/resource.go index 3a51dc40..01b79900 100644 --- a/stackit/services/logme/instance/resource.go +++ b/stackit/services/logme/instance/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -82,7 +81,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -101,12 +100,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "logme zone client configured") r.client = apiClient + tflog.Info(ctx, "LogMe instance client configured") } // Schema defines the schema for the resource. @@ -233,11 +232,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -247,6 +241,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters) if err != nil { @@ -268,58 +268,66 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*logme.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "logme instance created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "LogMe instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "logme instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "LogMe instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -335,11 +343,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -349,16 +352,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := logme.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -368,19 +377,23 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*logme.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "logme instance updated") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "LogMe instance updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -400,7 +413,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = logme.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -408,7 +421,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "logme instance deleted") + tflog.Info(ctx, "LogMe instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -417,8 +430,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -608,12 +621,11 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*logme.UpdateIn }, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list LogMe offerings", err.Error()) - return + return fmt.Errorf("getting LogMe offerings: %w", err) } version := model.Version.ValueString() @@ -634,26 +646,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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 *logme.APIClient, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *logme.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list LogMe offerings", err.Error()) - return + return fmt.Errorf("getting LogMe offerings: %w", err) } for _, offer := range *res.Offerings { @@ -661,10 +671,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *logme.APIClient, diags if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/mariadb/credentials/datasource.go b/stackit/services/mariadb/credentials/datasource.go index de7974c0..a878faab 100644 --- a/stackit/services/mariadb/credentials/datasource.go +++ b/stackit/services/mariadb/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,19 +64,19 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "mariadb credentials client configured") } // Schema defines the schema for the resource. func (r *credentialsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ "main": "MariaDB credentials data source schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", "credentials_id": "The credentials ID.", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT project ID to which the instance is associated.", @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "MariaDB credentials read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "mariadb credentials read") } diff --git a/stackit/services/mariadb/credentials/resource.go b/stackit/services/mariadb/credentials/resource.go index 9ee5092c..54557b58 100644 --- a/stackit/services/mariadb/credentials/resource.go +++ b/stackit/services/mariadb/credentials/resource.go @@ -25,9 +25,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &mariaDBCredentialsResource{} - _ resource.ResourceWithConfigure = &mariaDBCredentialsResource{} - _ resource.ResourceWithImportState = &mariaDBCredentialsResource{} + _ resource.Resource = &credentialsResource{} + _ resource.ResourceWithConfigure = &credentialsResource{} + _ resource.ResourceWithImportState = &credentialsResource{} ) type Model struct { @@ -45,23 +45,23 @@ type Model struct { Username types.String `tfsdk:"username"` } -// NewPostgreSQLCredentialsResource is a helper function to simplify the provider implementation. +// NewCredentialsResource is a helper function to simplify the provider implementation. func NewCredentialsResource() resource.Resource { - return &mariaDBCredentialsResource{} + return &credentialsResource{} } // credentialsResource is the resource implementation. -type mariaDBCredentialsResource struct { +type credentialsResource struct { client *mariadb.APIClient } // Metadata returns the resource type name. -func (r *mariaDBCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *credentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_mariadb_credentials" } // Configure adds the provider configured client to the resource. -func (r *mariaDBCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *credentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -69,7 +69,7 @@ func (r *mariaDBCredentialsResource) Configure(ctx context.Context, req resource providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,19 +88,19 @@ func (r *mariaDBCredentialsResource) Configure(ctx context.Context, req resource } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "MariaDB client configured") r.client = apiClient + tflog.Info(ctx, "MariaDB credentials client configured") } // Schema defines the schema for the resource. -func (r *mariaDBCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *credentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "MariaDB credentials resource schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", "credentials_id": "The credentials ID.", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT Project ID to which the instance is associated.", @@ -182,7 +182,7 @@ func (r *mariaDBCredentialsResource) Schema(_ context.Context, _ resource.Schema } // Create creates the resource and sets the initial Terraform state. -func (r *mariaDBCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -214,23 +214,26 @@ func (r *mariaDBCredentialsResource) Create(ctx context.Context, req resource.Cr } got, ok := wr.(*mariadb.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "MariaDB credentials created") } // Read refreshes the Terraform state with the latest data. -func (r *mariaDBCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -246,31 +249,34 @@ func (r *mariaDBCredentialsResource) Read(ctx context.Context, req resource.Read recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "MariaDB credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *mariaDBCredentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *mariaDBCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -288,7 +294,7 @@ func (r *mariaDBCredentialsResource) Delete(ctx context.Context, req resource.De // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = mariadb.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -299,12 +305,12 @@ func (r *mariaDBCredentialsResource) Delete(ctx context.Context, req resource.De } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credentials_id -func (r *mariaDBCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +// The expected format of the resource import identifier is: project_id,instance_id,credentials_id +func (r *credentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return @@ -313,7 +319,7 @@ func (r *mariaDBCredentialsResource) ImportState(ctx context.Context, req resour resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_id"), idParts[2])...) - tflog.Info(ctx, "Postgresql credentials state imported") + tflog.Info(ctx, "MariaDB credentials state imported") } func mapFields(credentialsResp *mariadb.CredentialsResponse, model *Model) error { diff --git a/stackit/services/mariadb/instance/datasource.go b/stackit/services/mariadb/instance/datasource.go index 9d24b20b..63233d0c 100644 --- a/stackit/services/mariadb/instance/datasource.go +++ b/stackit/services/mariadb/instance/datasource.go @@ -44,7 +44,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,19 +63,19 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "MariaDB zone client configured") r.client = apiClient + tflog.Info(ctx, "MariaDB zone client configured") } // Schema defines the schema for the resource. func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { descriptions := map[string]string{ "main": "MariaDB instance data source schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`\".", "instance_id": "ID of the MariaDB instance.", "project_id": "STACKIT Project ID to which the instance is associated.", "name": "Instance name.", @@ -152,37 +152,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "MariaDB instance read") } diff --git a/stackit/services/mariadb/instance/resource.go b/stackit/services/mariadb/instance/resource.go index d2d9d63b..67446387 100644 --- a/stackit/services/mariadb/instance/resource.go +++ b/stackit/services/mariadb/instance/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -82,7 +81,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -101,12 +100,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "mariadb zone client configured") r.client = apiClient + tflog.Info(ctx, "MariaDB instance client configured") } // Schema defines the schema for the resource. @@ -128,6 +127,9 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "id": schema.StringAttribute{ Description: descriptions["id"], Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "instance_id": schema.StringAttribute{ Description: descriptions["instance_id"], @@ -187,18 +189,33 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r }, "cf_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_space_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "dashboard_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "image_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_organization_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } @@ -215,11 +232,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -229,6 +241,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters) if err != nil { @@ -250,58 +268,66 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*mariadb.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "mariadb instance created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "MariaDB instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "mariadb instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "MariaDB instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -317,11 +343,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -331,16 +352,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := mariadb.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -350,23 +377,28 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*mariadb.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "mariadb instance updated") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "MariaDB instance updated") } // Delete deletes the resource and removes the Terraform state on success. func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -381,7 +413,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = mariadb.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -389,7 +421,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "mariadb instance deleted") + tflog.Info(ctx, "MariaDB instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -398,8 +430,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -589,12 +621,11 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*mariadb.Update }, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list MariaDB offerings", err.Error()) - return + return fmt.Errorf("getting MariaDB offerings: %w", err) } version := model.Version.ValueString() @@ -615,26 +646,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list MariaDB offerings", err.Error()) - return + return fmt.Errorf("getting MariaDB offerings: %w", err) } for _, offer := range *res.Offerings { @@ -642,10 +671,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, diag if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/opensearch/credentials/datasource.go b/stackit/services/opensearch/credentials/datasource.go index a9010a3b..00747393 100644 --- a/stackit/services/opensearch/credentials/datasource.go +++ b/stackit/services/opensearch/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "OpenSearch credentials client configured") } // Schema defines the schema for the resource. @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql credentials read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "OpenSearch credentials read") } diff --git a/stackit/services/opensearch/credentials/resource.go b/stackit/services/opensearch/credentials/resource.go index e1560817..d12862ac 100644 --- a/stackit/services/opensearch/credentials/resource.go +++ b/stackit/services/opensearch/credentials/resource.go @@ -25,9 +25,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &openSearchCredentialsResource{} - _ resource.ResourceWithConfigure = &openSearchCredentialsResource{} - _ resource.ResourceWithImportState = &openSearchCredentialsResource{} + _ resource.Resource = &credentialsResource{} + _ resource.ResourceWithConfigure = &credentialsResource{} + _ resource.ResourceWithImportState = &credentialsResource{} ) type Model struct { @@ -47,21 +47,21 @@ type Model struct { // NewCredentialsResource is a helper function to simplify the provider implementation. func NewCredentialsResource() resource.Resource { - return &openSearchCredentialsResource{} + return &credentialsResource{} } // credentialsResource is the resource implementation. -type openSearchCredentialsResource struct { +type credentialsResource struct { client *opensearch.APIClient } // Metadata returns the resource type name. -func (r *openSearchCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *credentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_opensearch_credentials" } // Configure adds the provider configured client to the resource. -func (r *openSearchCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *credentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -69,7 +69,7 @@ func (r *openSearchCredentialsResource) Configure(ctx context.Context, req resou providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,16 +88,16 @@ func (r *openSearchCredentialsResource) Configure(ctx context.Context, req resou } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "OpenSearch zone client configured") r.client = apiClient + tflog.Info(ctx, "OpenSearch credentials client configured") } // Schema defines the schema for the resource. -func (r *openSearchCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *credentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "OpenSearch credentials resource schema.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", @@ -182,7 +182,7 @@ func (r *openSearchCredentialsResource) Schema(_ context.Context, _ resource.Sch } // Create creates the resource and sets the initial Terraform state. -func (r *openSearchCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -214,23 +214,26 @@ func (r *openSearchCredentialsResource) Create(ctx context.Context, req resource } got, ok := wr.(*opensearch.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "OpenSearch credentials created") } // Read refreshes the Terraform state with the latest data. -func (r *openSearchCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -246,31 +249,34 @@ func (r *openSearchCredentialsResource) Read(ctx context.Context, req resource.R recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "OpenSearch credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *openSearchCredentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *openSearchCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -288,7 +294,7 @@ func (r *openSearchCredentialsResource) Delete(ctx context.Context, req resource // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = opensearch.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -300,11 +306,11 @@ func (r *openSearchCredentialsResource) Delete(ctx context.Context, req resource // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,instance_id,credentials_id -func (r *openSearchCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *credentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return diff --git a/stackit/services/opensearch/instance/datasource.go b/stackit/services/opensearch/instance/datasource.go index 6888cd8e..885ce439 100644 --- a/stackit/services/opensearch/instance/datasource.go +++ b/stackit/services/opensearch/instance/datasource.go @@ -44,7 +44,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,12 +63,12 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "OpenSearch zone client configured") r.client = apiClient + tflog.Info(ctx, "OpenSearch zone client configured") } // Schema defines the schema for the resource. @@ -152,37 +152,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "OpenSearch instance read") } diff --git a/stackit/services/opensearch/instance/resource.go b/stackit/services/opensearch/instance/resource.go index ee0a8470..79191281 100644 --- a/stackit/services/opensearch/instance/resource.go +++ b/stackit/services/opensearch/instance/resource.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -82,7 +81,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -101,12 +100,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "opensearch zone client configured") r.client = apiClient + tflog.Info(ctx, "OpenSearch instance client configured") } // Schema defines the schema for the resource. @@ -128,6 +127,9 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "id": schema.StringAttribute{ Description: descriptions["id"], Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "instance_id": schema.StringAttribute{ Description: descriptions["instance_id"], @@ -187,18 +189,33 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r }, "cf_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_space_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "dashboard_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "image_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_organization_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } @@ -215,11 +232,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -229,6 +241,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters) if err != nil { @@ -250,58 +268,66 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*opensearch.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "opensearch instance created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "OpenSearch instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "opensearch instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "OpenSearch instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -317,11 +343,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -331,16 +352,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := opensearch.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -350,19 +377,23 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*opensearch.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "opensearch instance updated") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "OpenSearch instance updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -382,7 +413,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = opensearch.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -390,7 +421,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "opensearch instance deleted") + tflog.Info(ctx, "OpenSearch instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -399,8 +430,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -410,6 +441,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) tflog.Info(ctx, "OpenSearch instance state imported") } + func mapFields(instance *opensearch.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") @@ -442,6 +474,7 @@ func mapFields(instance *opensearch.Instance, model *Model) error { model.ImageUrl = types.StringPointerValue(instance.ImageUrl) model.Name = types.StringPointerValue(instance.Name) model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) + if instance.Parameters == nil { model.Parameters = types.ObjectNull(parametersTypes) } else { @@ -588,12 +621,11 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*opensearch.Upd }, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list OpenSearch offerings", err.Error()) - return + return fmt.Errorf("getting OpenSearch offerings: %w", err) } version := model.Version.ValueString() @@ -614,26 +646,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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 *opensearch.APIClient, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *opensearch.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list OpenSearch offerings", err.Error()) - return + return fmt.Errorf("getting OpenSearch offerings: %w", err) } for _, offer := range *res.Offerings { @@ -641,10 +671,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *opensearch.APIClient, d if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/postgresflex/instance/datasource.go b/stackit/services/postgresflex/instance/datasource.go index 57c5b088..a57b3032 100644 --- a/stackit/services/postgresflex/instance/datasource.go +++ b/stackit/services/postgresflex/instance/datasource.go @@ -46,7 +46,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -65,12 +65,12 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresflex instance client configured") r.client = apiClient + tflog.Info(ctx, "PostgresFlex instance client configured") } // Schema defines the schema for the resource. @@ -159,47 +159,50 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } var flavor = &flavorModel{} - if !(state.Flavor.IsNull() || state.Flavor.IsUnknown()) { - diags = state.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) + if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } var storage = &storageModel{} - if !(state.Storage.IsNull() || state.Storage.IsUnknown()) { - diags = state.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) + if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } - err = mapFields(instanceResp, &state, flavor, storage) + err = mapFields(instanceResp, &model, flavor, storage) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex instance read") } diff --git a/stackit/services/postgresflex/instance/resource.go b/stackit/services/postgresflex/instance/resource.go index fa8e288a..2a9b1960 100644 --- a/stackit/services/postgresflex/instance/resource.go +++ b/stackit/services/postgresflex/instance/resource.go @@ -99,7 +99,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -118,12 +118,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresflex instance client configured") r.client = apiClient + tflog.Info(ctx, "PostgresFlex instance client configured") } // Schema defines the schema for the resource. @@ -286,10 +286,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } - if createResp == nil || createResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "Didn't get ID of created instance. An instance might have been created") - return - } instanceId := *createResp.Id ctx = tflog.SetField(ctx, "instance_id", instanceId) wr, err := postgresflex.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -299,46 +295,49 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*postgresflex.InstanceResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model, flavor, storage) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresflex instance created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var flavor = &flavorModel{} - if !(state.Flavor.IsNull() || state.Flavor.IsUnknown()) { - diags = state.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) + if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } } var storage = &storageModel{} - if !(state.Storage.IsNull() || state.Storage.IsUnknown()) { - diags = state.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) + if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -351,16 +350,19 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state, flavor, storage) + // Map response body to schema + err = mapFields(instanceResp, &model, flavor, storage) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresflex instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -409,7 +411,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques // Generate API request body from model payload, err := toUpdatePayload(&model, acl, flavor, storage) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance @@ -425,11 +427,11 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*postgresflex.InstanceResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model, flavor, storage) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) @@ -437,6 +439,9 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Postgresflex instance updated") } @@ -457,7 +462,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = postgresflex.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -465,7 +470,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "Postgresflex instance deleted") + tflog.Info(ctx, "PostgresFlex instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -474,8 +479,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return diff --git a/stackit/services/postgresflex/user/datasource.go b/stackit/services/postgresflex/user/datasource.go index eaa7cecf..08de46a7 100644 --- a/stackit/services/postgresflex/user/datasource.go +++ b/stackit/services/postgresflex/user/datasource.go @@ -45,7 +45,7 @@ func (r *userDataSource) Configure(ctx context.Context, req datasource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *userDataSource) Configure(ctx context.Context, req datasource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresflex user client configured") r.client = apiClient + tflog.Info(ctx, "PostgresFlex user client configured") } // Schema defines the schema for the resource. @@ -150,19 +150,22 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql user read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex user read") } diff --git a/stackit/services/postgresflex/user/resource.go b/stackit/services/postgresflex/user/resource.go index 2e7da20e..a42d799c 100644 --- a/stackit/services/postgresflex/user/resource.go +++ b/stackit/services/postgresflex/user/resource.go @@ -69,7 +69,7 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,12 +88,12 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresflex user client configured") r.client = apiClient + tflog.Info(ctx, "PostgresFlex user client configured") } // Schema defines the schema for the resource. @@ -217,22 +217,25 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r return } if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "Didn't get ID of created user. A user might have been created") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") return } userId := *userResp.Item.Id ctx = tflog.SetField(ctx, "user_id", userId) - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFieldsCreate(userResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresflex user created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex user created") } // Read refreshes the Terraform state with the latest data. @@ -252,27 +255,30 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresflex user read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgresFlex user read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating user", "user can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -295,14 +301,14 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r // Delete existing record set err := r.client.DeleteUser(ctx, projectId, instanceId, userId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) } _, err = postgresflex.DeleteUserWaitHandler(ctx, r.client, projectId, instanceId, userId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "Postgresflex user deleted") + tflog.Info(ctx, "PostgresFlex user deleted") } // ImportState imports a resource into the Terraform state on success. @@ -311,7 +317,7 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing user", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[user_id], got %q", req.ID), ) return diff --git a/stackit/services/postgresql/credentials/datasource.go b/stackit/services/postgresql/credentials/datasource.go index a55b88b6..e4a03c4f 100644 --- a/stackit/services/postgresql/credentials/datasource.go +++ b/stackit/services/postgresql/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "PostgreSQL credentials client configured") } // Schema defines the schema for the resource. @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql credentials read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL credentials read") } diff --git a/stackit/services/postgresql/credentials/resource.go b/stackit/services/postgresql/credentials/resource.go index c6a58189..76a55e23 100644 --- a/stackit/services/postgresql/credentials/resource.go +++ b/stackit/services/postgresql/credentials/resource.go @@ -69,7 +69,7 @@ func (r *credentialsResource) Configure(ctx context.Context, req resource.Config providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,12 +88,12 @@ func (r *credentialsResource) Configure(ctx context.Context, req resource.Config } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "PostgreSQL credentials client configured") } // Schema defines the schema for the resource. @@ -214,19 +214,22 @@ func (r *credentialsResource) Create(ctx context.Context, req resource.CreateReq } got, ok := wr.(*postgresql.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql credentials created") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL credentials created") } // Read refreshes the Terraform state with the latest data. @@ -246,27 +249,30 @@ func (r *credentialsResource) Read(ctx context.Context, req resource.ReadRequest recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql credentials read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -288,14 +294,14 @@ func (r *credentialsResource) Delete(ctx context.Context, req resource.DeleteReq // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = postgresql.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "Postgresql credentials deleted") + tflog.Info(ctx, "PostgreSQL credentials deleted") } // ImportState imports a resource into the Terraform state on success. @@ -304,7 +310,7 @@ func (r *credentialsResource) ImportState(ctx context.Context, req resource.Impo idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return @@ -313,7 +319,7 @@ func (r *credentialsResource) ImportState(ctx context.Context, req resource.Impo resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_id"), idParts[2])...) - tflog.Info(ctx, "Postgresql credentials state imported") + tflog.Info(ctx, "PostgreSQL credentials state imported") } func mapFields(credentialsResp *postgresql.CredentialsResponse, model *Model) error { diff --git a/stackit/services/postgresql/instance/datasource.go b/stackit/services/postgresql/instance/datasource.go index 181813d9..f9afabc0 100644 --- a/stackit/services/postgresql/instance/datasource.go +++ b/stackit/services/postgresql/instance/datasource.go @@ -6,12 +6,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/validate" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/postgresql" ) @@ -45,7 +45,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "PostgreSQL zone client configured") } // Schema defines the schema for the resource. @@ -169,37 +169,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL instance read") } diff --git a/stackit/services/postgresql/instance/resource.go b/stackit/services/postgresql/instance/resource.go index e3b0d0c0..a4ba7a72 100644 --- a/stackit/services/postgresql/instance/resource.go +++ b/stackit/services/postgresql/instance/resource.go @@ -8,21 +8,21 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/terraform-provider-stackit/stackit/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/validate" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/postgresql" - "github.com/stackitcloud/terraform-provider-stackit/stackit/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -92,7 +92,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -111,12 +111,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Postgresql zone client configured") r.client = apiClient + tflog.Info(ctx, "PostgreSQL instance client configured") } // Schema defines the schema for the resource. @@ -250,7 +250,6 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r // Create creates the resource and sets the initial Terraform state. func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -260,11 +259,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} var parametersPlugins *[]string if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { @@ -288,6 +282,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters, parametersPlugins) if err != nil { @@ -309,19 +309,23 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*postgresql.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Postgresql instance created") } @@ -352,40 +356,44 @@ func toCreatePayload(model *Model, parameters *parametersModel, parametersPlugin // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -401,11 +409,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} var parametersPlugins *[]string if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { @@ -426,16 +429,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters, parametersPlugins) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := postgresql.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -445,19 +454,23 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*postgresql.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "Postgresql instance updated") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "PostgreSQL instance updated") } func toUpdatePayload(model *Model, parameters *parametersModel, parametersPlugins *[]string) (*postgresql.UpdateInstancePayload, error) { @@ -500,7 +513,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = postgresql.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -508,7 +521,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "Postgresql instance deleted") + tflog.Info(ctx, "PostgreSQL instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -517,8 +530,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -526,7 +539,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "Postgresql instance state imported") + tflog.Info(ctx, "PostgreSQL instance state imported") } func mapFields(instance *postgresql.Instance, model *Model) error { @@ -669,12 +682,11 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { return output, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list PostgreSQL offerings", err.Error()) - return + return fmt.Errorf("getting PostgreSQL offerings: %w", err) } version := model.Version.ValueString() @@ -695,26 +707,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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 *postgresql.APIClient, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *postgresql.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list PostgreSQL offerings", err.Error()) - return + return fmt.Errorf("getting PostgreSQL offerings: %w", err) } for _, offer := range *res.Offerings { @@ -722,10 +732,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *postgresql.APIClient, d if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/rabbitmq/credentials/datasource.go b/stackit/services/rabbitmq/credentials/datasource.go index 51585685..a87f3aea 100644 --- a/stackit/services/rabbitmq/credentials/datasource.go +++ b/stackit/services/rabbitmq/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "RabbitMQ zone client configured") r.client = apiClient + tflog.Info(ctx, "RabbitMQ credentials client configured") } // Schema defines the schema for the resource. @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "RabbitMQ credentials read") } diff --git a/stackit/services/rabbitmq/credentials/resource.go b/stackit/services/rabbitmq/credentials/resource.go index 9de8cc90..8d45ea7d 100644 --- a/stackit/services/rabbitmq/credentials/resource.go +++ b/stackit/services/rabbitmq/credentials/resource.go @@ -25,9 +25,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &rabbitMQCredentialsResource{} - _ resource.ResourceWithConfigure = &rabbitMQCredentialsResource{} - _ resource.ResourceWithImportState = &rabbitMQCredentialsResource{} + _ resource.Resource = &credentialsResource{} + _ resource.ResourceWithConfigure = &credentialsResource{} + _ resource.ResourceWithImportState = &credentialsResource{} ) type Model struct { @@ -47,21 +47,21 @@ type Model struct { // NewCredentialsResource is a helper function to simplify the provider implementation. func NewCredentialsResource() resource.Resource { - return &rabbitMQCredentialsResource{} + return &credentialsResource{} } // credentialsResource is the resource implementation. -type rabbitMQCredentialsResource struct { +type credentialsResource struct { client *rabbitmq.APIClient } // Metadata returns the resource type name. -func (r *rabbitMQCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *credentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_rabbitmq_credentials" } // Configure adds the provider configured client to the resource. -func (r *rabbitMQCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *credentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -69,7 +69,7 @@ func (r *rabbitMQCredentialsResource) Configure(ctx context.Context, req resourc providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,16 +88,16 @@ func (r *rabbitMQCredentialsResource) Configure(ctx context.Context, req resourc } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "RabbitMQ zone client configured") r.client = apiClient + tflog.Info(ctx, "RabbitMQ credentials client configured") } // Schema defines the schema for the resource. -func (r *rabbitMQCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *credentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "RabbitMQ credentials resource schema.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", @@ -182,7 +182,7 @@ func (r *rabbitMQCredentialsResource) Schema(_ context.Context, _ resource.Schem } // Create creates the resource and sets the initial Terraform state. -func (r *rabbitMQCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -214,23 +214,26 @@ func (r *rabbitMQCredentialsResource) Create(ctx context.Context, req resource.C } got, ok := wr.(*rabbitmq.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "RabbitMQ credentials created") } // Read refreshes the Terraform state with the latest data. -func (r *rabbitMQCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -246,31 +249,34 @@ func (r *rabbitMQCredentialsResource) Read(ctx context.Context, req resource.Rea recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "RabbitMQ credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *rabbitMQCredentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *rabbitMQCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -288,7 +294,7 @@ func (r *rabbitMQCredentialsResource) Delete(ctx context.Context, req resource.D // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = rabbitmq.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -300,11 +306,11 @@ func (r *rabbitMQCredentialsResource) Delete(ctx context.Context, req resource.D // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,instance_id,credentials_id -func (r *rabbitMQCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *credentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return diff --git a/stackit/services/rabbitmq/instance/datasource.go b/stackit/services/rabbitmq/instance/datasource.go index dad85ee2..fc7dbc9c 100644 --- a/stackit/services/rabbitmq/instance/datasource.go +++ b/stackit/services/rabbitmq/instance/datasource.go @@ -44,7 +44,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,12 +63,12 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "RabbitMQ zone client configured") r.client = apiClient + tflog.Info(ctx, "RabbitMQ zone client configured") } // Schema defines the schema for the resource. @@ -152,37 +152,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, &state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "RabbitMQ instance read") } diff --git a/stackit/services/rabbitmq/instance/resource.go b/stackit/services/rabbitmq/instance/resource.go index 0cd3b17d..9ea6ef79 100644 --- a/stackit/services/rabbitmq/instance/resource.go +++ b/stackit/services/rabbitmq/instance/resource.go @@ -8,8 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -83,7 +81,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -102,12 +100,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "rabbitmq zone client configured") r.client = apiClient + tflog.Info(ctx, "RabbitMQ instance client configured") } // Schema defines the schema for the resource. @@ -184,34 +182,40 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "sgw_acl": schema.StringAttribute{ Optional: true, Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, }, }, Optional: true, Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, }, "cf_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_space_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "dashboard_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "image_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_organization_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } @@ -228,11 +232,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -242,6 +241,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters) if err != nil { @@ -263,79 +268,66 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*rabbitmq.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "rabbitmq instance created") -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*rabbitmq.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if parameters == nil { - return &rabbitmq.CreateInstancePayload{ - InstanceName: model.Name.ValueStringPointer(), - PlanId: model.PlanId.ValueStringPointer(), - }, nil - } - payloadParams := &rabbitmq.InstanceParameters{} - if parameters.SgwAcl.ValueString() != "" { - payloadParams.SgwAcl = parameters.SgwAcl.ValueStringPointer() + if resp.Diagnostics.HasError() { + return } - return &rabbitmq.CreateInstancePayload{ - InstanceName: model.Name.ValueStringPointer(), - Parameters: payloadParams, - PlanId: model.PlanId.ValueStringPointer(), - }, nil + tflog.Info(ctx, "RabbitMQ instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "rabbitmq instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "RabbitMQ instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -351,11 +343,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -365,16 +352,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := rabbitmq.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -384,41 +377,28 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*rabbitmq.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "rabbitmq instance updated") -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*rabbitmq.UpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - if parameters == nil { - return &rabbitmq.UpdateInstancePayload{ - PlanId: model.PlanId.ValueStringPointer(), - }, nil + if resp.Diagnostics.HasError() { + return } - return &rabbitmq.UpdateInstancePayload{ - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: parameters.SgwAcl.ValueStringPointer(), - }, - PlanId: model.PlanId.ValueStringPointer(), - }, nil + tflog.Info(ctx, "RabbitMQ instance updated") } // Delete deletes the resource and removes the Terraform state on success. func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -433,7 +413,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = rabbitmq.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -441,7 +421,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "rabbitmq instance deleted") + tflog.Info(ctx, "RabbitMQ instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -450,8 +430,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -602,12 +582,50 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { return output, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func toCreatePayload(model *Model, parameters *parametersModel) (*rabbitmq.CreateInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if parameters == nil { + return &rabbitmq.CreateInstancePayload{ + InstanceName: model.Name.ValueStringPointer(), + PlanId: model.PlanId.ValueStringPointer(), + }, nil + } + payloadParams := &rabbitmq.InstanceParameters{} + if parameters.SgwAcl.ValueString() != "" { + payloadParams.SgwAcl = parameters.SgwAcl.ValueStringPointer() + } + return &rabbitmq.CreateInstancePayload{ + InstanceName: model.Name.ValueStringPointer(), + Parameters: payloadParams, + PlanId: model.PlanId.ValueStringPointer(), + }, nil +} + +func toUpdatePayload(model *Model, parameters *parametersModel) (*rabbitmq.UpdateInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + if parameters == nil { + return &rabbitmq.UpdateInstancePayload{ + PlanId: model.PlanId.ValueStringPointer(), + }, nil + } + return &rabbitmq.UpdateInstancePayload{ + Parameters: &rabbitmq.InstanceParameters{ + SgwAcl: parameters.SgwAcl.ValueStringPointer(), + }, + PlanId: model.PlanId.ValueStringPointer(), + }, nil +} + +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list RabbitMQ offerings", err.Error()) - return + return fmt.Errorf("getting RabbitMQ offerings: %w", err) } version := model.Version.ValueString() @@ -628,26 +646,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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 *rabbitmq.APIClient, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *rabbitmq.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list RabbitMQ offerings", err.Error()) - return + return fmt.Errorf("getting RabbitMQ offerings: %w", err) } for _, offer := range *res.Offerings { @@ -655,10 +671,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *rabbitmq.APIClient, dia if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/redis/credentials/datasource.go b/stackit/services/redis/credentials/datasource.go index a7bcb8ba..56b9f7dc 100644 --- a/stackit/services/redis/credentials/datasource.go +++ b/stackit/services/redis/credentials/datasource.go @@ -45,7 +45,7 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *credentialsDataSource) Configure(ctx context.Context, req datasource.Co } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Redis zone client configured") r.client = apiClient + tflog.Info(ctx, "Redis credentials client configured") } // Schema defines the schema for the resource. @@ -160,19 +160,22 @@ func (r *credentialsDataSource) Read(ctx context.Context, req datasource.ReadReq recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Redis credentials read") } diff --git a/stackit/services/redis/credentials/resource.go b/stackit/services/redis/credentials/resource.go index 5844d42d..c8b98694 100644 --- a/stackit/services/redis/credentials/resource.go +++ b/stackit/services/redis/credentials/resource.go @@ -6,13 +6,13 @@ import ( "strings" "time" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/validate" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -25,9 +25,9 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &postgresCredentialsResource{} - _ resource.ResourceWithConfigure = &postgresCredentialsResource{} - _ resource.ResourceWithImportState = &postgresCredentialsResource{} + _ resource.Resource = &redisCredentialsResource{} + _ resource.ResourceWithConfigure = &redisCredentialsResource{} + _ resource.ResourceWithImportState = &redisCredentialsResource{} ) type Model struct { @@ -47,21 +47,21 @@ type Model struct { // NewCredentialsResource is a helper function to simplify the provider implementation. func NewCredentialsResource() resource.Resource { - return &postgresCredentialsResource{} + return &redisCredentialsResource{} } // credentialsResource is the resource implementation. -type postgresCredentialsResource struct { +type redisCredentialsResource struct { client *redis.APIClient } // Metadata returns the resource type name. -func (r *postgresCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *redisCredentialsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_redis_credentials" } // Configure adds the provider configured client to the resource. -func (r *postgresCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *redisCredentialsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -69,7 +69,7 @@ func (r *postgresCredentialsResource) Configure(ctx context.Context, req resourc providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -88,16 +88,16 @@ func (r *postgresCredentialsResource) Configure(ctx context.Context, req resourc } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Redis zone client configured") r.client = apiClient + tflog.Info(ctx, "Redis credentials client configured") } // Schema defines the schema for the resource. -func (r *postgresCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *redisCredentialsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "Redis credentials resource schema.", "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credentials_id`\".", @@ -182,7 +182,7 @@ func (r *postgresCredentialsResource) Schema(_ context.Context, _ resource.Schem } // Create creates the resource and sets the initial Terraform state. -func (r *postgresCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *redisCredentialsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -214,23 +214,26 @@ func (r *postgresCredentialsResource) Create(ctx context.Context, req resource.C } got, ok := wr.(*redis.CredentialsResponse) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Redis credentials created") } // Read refreshes the Terraform state with the latest data. -func (r *postgresCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *redisCredentialsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -246,31 +249,34 @@ func (r *postgresCredentialsResource) Read(ctx context.Context, req resource.Rea recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(recordSetResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials", fmt.Sprintf("Processing API payload: %v", err)) return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Redis credentials read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *postgresCredentialsResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *redisCredentialsResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating credentials", "credentials can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials", "Credentials can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *postgresCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *redisCredentialsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -288,7 +294,7 @@ func (r *postgresCredentialsResource) Delete(ctx context.Context, req resource.D // Delete existing record set err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials", fmt.Sprintf("Calling API: %v", err)) } _, err = redis.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialsId).SetTimeout(1 * time.Minute).WaitWithContext(ctx) if err != nil { @@ -300,11 +306,11 @@ func (r *postgresCredentialsResource) Delete(ctx context.Context, req resource.D // ImportState imports a resource into the Terraform state on success. // The expected format of the resource import identifier is: project_id,instance_id,credentials_id -func (r *postgresCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *redisCredentialsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, - "Unexpected Import Identifier", + "Error importing credentials", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credentials_id], got %q", req.ID), ) return diff --git a/stackit/services/redis/instance/datasource.go b/stackit/services/redis/instance/datasource.go index 662c5f00..e97cd252 100644 --- a/stackit/services/redis/instance/datasource.go +++ b/stackit/services/redis/instance/datasource.go @@ -44,7 +44,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,12 +63,12 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Redis zone client configured") r.client = apiClient + tflog.Info(ctx, "Redis zone client configured") } // Schema defines the schema for the resource. @@ -152,37 +152,41 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques // Read refreshes the Terraform state with the latest data. func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) + instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - err = mapFields(instanceResp, &state) + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Redis instance read") } diff --git a/stackit/services/redis/instance/resource.go b/stackit/services/redis/instance/resource.go index 77496650..abb9959a 100644 --- a/stackit/services/redis/instance/resource.go +++ b/stackit/services/redis/instance/resource.go @@ -8,8 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -83,7 +81,7 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -102,12 +100,12 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "redis client configured") r.client = apiClient + tflog.Info(ctx, "Redis instance client configured") } // Schema defines the schema for the resource. @@ -129,6 +127,9 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "id": schema.StringAttribute{ Description: descriptions["id"], Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "instance_id": schema.StringAttribute{ Description: descriptions["instance_id"], @@ -181,34 +182,40 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "sgw_acl": schema.StringAttribute{ Optional: true, Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, }, }, Optional: true, Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, }, "cf_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_space_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "dashboard_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "image_url": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "cf_organization_guid": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } @@ -225,11 +232,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -239,6 +241,12 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toCreatePayload(&model, parameters) if err != nil { @@ -260,79 +268,66 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } got, ok := wr.(*redis.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "redis instance created") -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*redis.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if parameters == nil { - return &redis.CreateInstancePayload{ - InstanceName: model.Name.ValueStringPointer(), - PlanId: model.PlanId.ValueStringPointer(), - }, nil - } - payloadParams := &redis.InstanceParameters{} - if parameters.SgwAcl.ValueString() != "" { - payloadParams.SgwAcl = parameters.SgwAcl.ValueStringPointer() + if resp.Diagnostics.HasError() { + return } - return &redis.CreateInstancePayload{ - InstanceName: model.Name.ValueStringPointer(), - Parameters: payloadParams, - PlanId: model.PlanId.ValueStringPointer(), - }, nil + tflog.Info(ctx, "Redis instance created") } // Read refreshes the Terraform state with the latest data. func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) + var model Model + diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() - instanceId := state.InstanceId.ValueString() + projectId := model.ProjectId.ValueString() + instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instances", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(instanceResp, &state) + // Map response body to schema + err = mapFields(instanceResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + 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 - loadPlanNameAndVersion(ctx, r.client, &resp.Diagnostics, &state) - if resp.Diagnostics.HasError() { + err = loadPlanNameAndVersion(ctx, r.client, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) return } // Set refreshed state - diags = resp.State.Set(ctx, state) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "redis instance read") + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Redis instance read") } // Update updates the resource and sets the updated Terraform state on success. @@ -348,11 +343,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) - r.loadPlanId(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - var parameters = ¶metersModel{} if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) @@ -362,16 +352,22 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } } + err := r.loadPlanId(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) + return + } + // Generate API request body from model payload, err := toUpdatePayload(&model, parameters) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing instance err = r.client.UpdateInstance(ctx, projectId, instanceId).UpdateInstancePayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } wr, err := redis.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -381,41 +377,28 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } got, ok := wr.(*redis.Instance) if !ok { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields in update", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) - tflog.Info(ctx, "redis instance updated") -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*redis.UpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - if parameters == nil { - return &redis.UpdateInstancePayload{ - PlanId: model.PlanId.ValueStringPointer(), - }, nil + if resp.Diagnostics.HasError() { + return } - return &redis.UpdateInstancePayload{ - Parameters: &redis.InstanceParameters{ - SgwAcl: parameters.SgwAcl.ValueStringPointer(), - }, - PlanId: model.PlanId.ValueStringPointer(), - }, nil + tflog.Info(ctx, "Redis instance updated") } // Delete deletes the resource and removes the Terraform state on success. func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) @@ -430,7 +413,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques // Delete existing instance err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } _, err = redis.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -438,7 +421,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } - tflog.Info(ctx, "redis instance deleted") + tflog.Info(ctx, "Redis instance deleted") } // ImportState imports a resource into the Terraform state on success. @@ -447,8 +430,8 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) return @@ -599,12 +582,50 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { return output, nil } -func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnostics, model *Model) { +func toCreatePayload(model *Model, parameters *parametersModel) (*redis.CreateInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if parameters == nil { + return &redis.CreateInstancePayload{ + InstanceName: model.Name.ValueStringPointer(), + PlanId: model.PlanId.ValueStringPointer(), + }, nil + } + payloadParams := &redis.InstanceParameters{} + if parameters.SgwAcl.ValueString() != "" { + payloadParams.SgwAcl = parameters.SgwAcl.ValueStringPointer() + } + return &redis.CreateInstancePayload{ + InstanceName: model.Name.ValueStringPointer(), + Parameters: payloadParams, + PlanId: model.PlanId.ValueStringPointer(), + }, nil +} + +func toUpdatePayload(model *Model, parameters *parametersModel) (*redis.UpdateInstancePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + if parameters == nil { + return &redis.UpdateInstancePayload{ + PlanId: model.PlanId.ValueStringPointer(), + }, nil + } + return &redis.UpdateInstancePayload{ + Parameters: &redis.InstanceParameters{ + SgwAcl: parameters.SgwAcl.ValueStringPointer(), + }, + PlanId: model.PlanId.ValueStringPointer(), + }, nil +} + +func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list Redis offerings", err.Error()) - return + return fmt.Errorf("getting Redis offerings: %w", err) } version := model.Version.ValueString() @@ -625,26 +646,24 @@ func (r *instanceResource) loadPlanId(ctx context.Context, diags *diag.Diagnosti } if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) - return + return nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } if !isValidVersion { - diags.AddError("Invalid version", fmt.Sprintf("Couldn't find version '%s', available versions are:%s", version, availableVersions)) - return + return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } - diags.AddError("Invalid plan_name", fmt.Sprintf("Couldn't find plan_name '%s' for version %s, available names are:%s", planName, version, availablePlanNames)) + 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 *redis.APIClient, diags *diag.Diagnostics, model *Model) { +func loadPlanNameAndVersion(ctx context.Context, client *redis.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() res, err := client.GetOfferings(ctx, projectId).Execute() if err != nil { - diags.AddError("Failed to list Redis offerings", err.Error()) - return + return fmt.Errorf("getting Redis offerings: %w", err) } for _, offer := range *res.Offerings { @@ -652,10 +671,10 @@ func loadPlanNameAndVersion(ctx context.Context, client *redis.APIClient, diags if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) - return + return nil } } } - diags.AddWarning("Failed to get plan_name and version", fmt.Sprintf("Couldn't find plan_name and version for plan_id = %s", planId)) + return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) } diff --git a/stackit/services/resourcemanager/project/datasource.go b/stackit/services/resourcemanager/project/datasource.go index a0451801..e05dac19 100644 --- a/stackit/services/resourcemanager/project/datasource.go +++ b/stackit/services/resourcemanager/project/datasource.go @@ -26,7 +26,7 @@ var ( _ datasource.DataSource = &projectDataSource{} ) -type ProjectData struct { +type ModelData struct { Id types.String `tfsdk:"id"` // needed by TF ContainerId types.String `tfsdk:"container_id"` ContainerParentId types.String `tfsdk:"parent_container_id"` @@ -60,7 +60,7 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -78,15 +78,12 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config ) } if err != nil { - resp.Diagnostics.AddError( - "Could not Configure API Client", - err.Error(), - ) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Resource Manager project client configured") d.client = apiClient + tflog.Info(ctx, "Resource Manager project client configured") } // Schema defines the schema for the data source. @@ -152,7 +149,7 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest // Read refreshes the Terraform state with the latest data. func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state ProjectData + var state ModelData diags := req.Config.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -163,13 +160,13 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest projectResp, err := d.client.GetProject(ctx, containerId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to Read Project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) return } err = mapDataFields(ctx, projectResp, &state) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, &state) @@ -180,7 +177,7 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest tflog.Info(ctx, "Resource Manager project read") } -func mapDataFields(ctx context.Context, projectResp *resourcemanager.ProjectResponseWithParents, model *ProjectData) (err error) { +func mapDataFields(ctx context.Context, projectResp *resourcemanager.ProjectResponseWithParents, model *ModelData) (err error) { if projectResp == nil { return fmt.Errorf("response input is nil") } diff --git a/stackit/services/resourcemanager/project/resource.go b/stackit/services/resourcemanager/project/resource.go index f06b8b42..ccd9241b 100644 --- a/stackit/services/resourcemanager/project/resource.go +++ b/stackit/services/resourcemanager/project/resource.go @@ -70,7 +70,7 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -92,12 +92,12 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "Resource Manager project client configured") r.client = apiClient + tflog.Info(ctx, "Resource Manager project client configured") } // Schema defines the schema for the resource. @@ -186,7 +186,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest serviceAccountEmail := r.client.GetConfig().ServiceAccountEmail if serviceAccountEmail == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", "The service account e-mail cannot be empty: set it in the provider configuration or through the STACKIT_SERVICE_ACCOUNT_EMAIL or in your credentials file (default filepath is ~/stackit/.credentials.json)") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", "The service account e-mail cannot be empty: set it in the provider configuration or through the STACKIT_SERVICE_ACCOUNT_EMAIL or in your credentials file (default filepath is ~/.stackit/credentials.json)") return } @@ -203,10 +203,6 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } respContainerId := *createResp.ContainerId - if respContainerId == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", "API didn't return project id") - return - } // If the request has not been processed yet and the containerId doesnt exist, // the waiter will fail with authentication error, so wait some time before checking the creation @@ -221,44 +217,50 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest return } - // Map response body to schema and populate Computed attribute values + // Map response body to schema err = mapFields(ctx, got, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Processing API payload: %v", err)) return } // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Resource Manager project created") } // Read refreshes the Terraform state with the latest data. func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state = &Model{} - diags := req.State.Get(ctx, state) + var model Model + diags := req.State.Get(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - containerId := state.ContainerId.ValueString() + containerId := model.ContainerId.ValueString() ctx = tflog.SetField(ctx, "container_id", containerId) projectResp, err := r.client.GetProject(ctx, containerId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) return } - // Map response body to schema and populate Computed attribute values - err = mapFields(ctx, projectResp, state) + // Map response body to schema + err = mapFields(ctx, projectResp, &model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API payload: %v", err)) return } - // Set refreshed state - diags = resp.State.Set(ctx, *state) + // Set refreshed model + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Resource Manager project read") } @@ -277,18 +279,32 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest // Generate API request body from model payload, err := toUpdatePayload(&model) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Could not create API payload: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Creating API payload: %v", err)) return } // Update existing project _, err = r.client.UpdateProject(ctx, containerId).UpdateProjectPayload(*payload).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Calling API: %v", err)) return } - diags = resp.State.Set(ctx, &model) + // Fetch updated zone + projectResp, err := r.client.GetProject(ctx, containerId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Calling API for updated data: %v", err)) + return + } + err = mapFields(ctx, projectResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "Resource Manager project updated") } @@ -308,7 +324,7 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest // Delete existing project err := r.client.DeleteProject(ctx, containerId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Calling API: %v", err)) return } @@ -326,8 +342,8 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 1 || idParts[0] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing project", fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), ) return diff --git a/stackit/services/ske/cluster/datasource.go b/stackit/services/ske/cluster/datasource.go index 1430c998..83ef3f0a 100644 --- a/stackit/services/ske/cluster/datasource.go +++ b/stackit/services/ske/cluster/datasource.go @@ -45,7 +45,7 @@ func (r *clusterDataSource) Configure(ctx context.Context, req datasource.Config providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -64,12 +64,12 @@ func (r *clusterDataSource) Configure(ctx context.Context, req datasource.Config } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "SKE client configured") r.client = apiClient + tflog.Info(ctx, "SKE client configured") } func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -292,19 +292,22 @@ func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest ctx = tflog.SetField(ctx, "name", name) clusterResp, err := r.client.GetCluster(ctx, projectId, name).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Unable to read cluster, project_id = %s, name = %s", projectId, name), err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Calling API: %v", err)) return } err = mapFields(ctx, clusterResp, &state) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err)) return } r.getCredential(ctx, &diags, &state) // Set refreshed state diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "SKE cluster read") } diff --git a/stackit/services/ske/cluster/resource.go b/stackit/services/ske/cluster/resource.go index 772932c7..3b059d24 100644 --- a/stackit/services/ske/cluster/resource.go +++ b/stackit/services/ske/cluster/resource.go @@ -148,7 +148,7 @@ func (r *clusterResource) Configure(ctx context.Context, req resource.ConfigureR providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -167,12 +167,12 @@ func (r *clusterResource) Configure(ctx context.Context, req resource.ConfigureR } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "SKE cluster client configured") r.client = apiClient + tflog.Info(ctx, "SKE cluster client configured") } // Schema defines the schema for the resource. @@ -213,7 +213,7 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: "Kubernetes version. Must only contain major and minor version (e.g. 1.22)", Required: true, Validators: []validator.String{ - validate.SemanticMinorVersion(), + validate.MinorVersionNumber(), }, }, "kubernetes_version_used": schema.StringAttribute{ @@ -502,8 +502,9 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "name", clusterName) - availableVersions := r.loadAvaiableVersions(ctx, &resp.Diagnostics) - if resp.Diagnostics.HasError() { + availableVersions, err := r.loadAvaiableVersions(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err)) return } @@ -512,34 +513,27 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest return } - // handle credential - r.getCredential(ctx, &resp.Diagnostics, &model) - if resp.Diagnostics.HasError() { - return - } - diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SKE cluster created") } -func (r *clusterResource) loadAvaiableVersions(ctx context.Context, diags *diag.Diagnostics) []ske.KubernetesVersion { +func (r *clusterResource) loadAvaiableVersions(ctx context.Context) ([]ske.KubernetesVersion, error) { c := r.client res, err := c.GetOptions(ctx).Execute() if err != nil { - diags.AddError("Failed loading cluster available versions: getting cluster options", err.Error()) - return nil + return nil, fmt.Errorf("calling API: %w", err) } if res.KubernetesVersions == nil { - diags.AddError("Failed loading cluster available versions: nil kubernetesVersions", err.Error()) - return nil + return nil, fmt.Errorf("API response has nil kubernetesVersions") } - return *res.KubernetesVersions + return *res.KubernetesVersions, nil } func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Cluster, availableVersions []ske.KubernetesVersion) { @@ -548,23 +542,22 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag name := model.Name.ValueString() kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableVersions) if err != nil { - diags.AddError("Failed to create cluster config payload", err.Error()) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err)) return } if hasDeprecatedVersion { - warningMessage := fmt.Sprintf("Using deprecated kubernetes version %s", *kubernetes.Version) - diags.AddWarning(warningMessage, "") + diags.AddWarning("Deprecated Kubernetes version", fmt.Sprintf("Version %s of Kubernetes is deprecated, please update it", *kubernetes.Version)) } nodePools := toNodepoolsPayload(ctx, model) maintenance, err := toMaintenancePayload(ctx, model) if err != nil { - diags.AddError("Failed to create maintenance payload", err.Error()) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating maintenance API payload: %v", err)) return } hibernations := toHibernationsPayload(model) extensions, err := toExtensionsPayload(ctx, model) if err != nil { - diags.AddError("Failed to create extension payload", err.Error()) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err)) return } @@ -577,35 +570,42 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag } _, err = r.client.CreateOrUpdateCluster(ctx, projectId, name).CreateOrUpdateClusterPayload(payload).Execute() if err != nil { - diags.AddError("failed during SKE create/update", err.Error()) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Calling API: %v", err)) return } wr, err := ske.CreateOrUpdateClusterWaitHandler(ctx, r.client, projectId, name).SetTimeout(30 * time.Minute).WaitWithContext(ctx) if err != nil { - diags.AddError("Error creating cluster", fmt.Sprintf("Cluster creation waiting: %v", err)) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Cluster creation waiting: %v", err)) return } got, ok := wr.(*ske.ClusterResponse) if !ok { - diags.AddError("Error creating cluster", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } err = mapFields(ctx, got, model) if err != nil { - diags.AddError("Mapping cluster fields", err.Error()) + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Handle credential + err = r.getCredential(ctx, model) + if err != nil { + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Getting credential: %v", err)) return } } -func (r *clusterResource) getCredential(ctx context.Context, diags *diag.Diagnostics, model *Cluster) { +func (r *clusterResource) getCredential(ctx context.Context, model *Cluster) error { c := r.client res, err := c.GetCredentials(ctx, model.ProjectId.ValueString(), model.Name.ValueString()).Execute() if err != nil { - diags.AddError("failed fetching cluster credentials", err.Error()) - return + return fmt.Errorf("fetching cluster credentials: %w", err) } model.KubeConfig = types.StringPointerValue(res.Kubeconfig) + return nil } func toNodepoolsPayload(ctx context.Context, m *Cluster) []ske.Nodepool { @@ -1079,17 +1079,20 @@ func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, re clResp, err := r.client.GetCluster(ctx, projectId, name).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Unable to read cluster, project_id = %s, name = %s", projectId, name), err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Calling API: %v", err)) return } err = mapFields(ctx, clResp, &state) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error mapping fields", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err)) return } diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "SKE cluster read") } @@ -1105,8 +1108,9 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "name", clName) - availableVersions := r.loadAvaiableVersions(ctx, &resp.Diagnostics) - if resp.Diagnostics.HasError() { + availableVersions, err := r.loadAvaiableVersions(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating cluster", fmt.Sprintf("Loading available Kubernetes versions: %v", err)) return } @@ -1115,14 +1119,11 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest return } - // handle credential - r.getCredential(ctx, &resp.Diagnostics, &model) + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) tflog.Info(ctx, "SKE cluster updated") } @@ -1140,7 +1141,7 @@ func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest c := r.client _, err := c.DeleteCluster(ctx, projectId, name).Execute() if err != nil { - resp.Diagnostics.AddError("failed deleting cluster", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Calling API: %v", err)) return } _, err = ske.DeleteClusterWaitHandler(ctx, r.client, projectId, name).SetTimeout(15 * time.Minute).WaitWithContext(ctx) @@ -1157,8 +1158,8 @@ func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportSt idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing cluster", fmt.Sprintf("Expected import identifier with format: [project_id],[name] Got: %q", req.ID), ) return diff --git a/stackit/services/ske/project/datasource.go b/stackit/services/ske/project/datasource.go index c3b2a46f..5e47db80 100644 --- a/stackit/services/ske/project/datasource.go +++ b/stackit/services/ske/project/datasource.go @@ -44,7 +44,7 @@ func (r *projectDataSource) Configure(ctx context.Context, req datasource.Config providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Data Source Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -63,12 +63,12 @@ func (r *projectDataSource) Configure(ctx context.Context, req datasource.Config } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "SKE client configured") r.client = apiClient + tflog.Info(ctx, "SKE client configured") } // Schema defines the schema for the resource. @@ -93,23 +93,26 @@ func (r *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest // Read refreshes the Terraform state with the latest data. func (r *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) + var model Model + diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - projectId := state.ProjectId.ValueString() + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) _, err := r.client.GetProject(ctx, projectId).Execute() if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Unable to read project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) return } - state.Id = types.StringValue(projectId) - state.ProjectId = types.StringValue(projectId) - diags = resp.State.Set(ctx, state) + model.Id = types.StringValue(projectId) + model.ProjectId = types.StringValue(projectId) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "SKE project read") } diff --git a/stackit/services/ske/project/resource.go b/stackit/services/ske/project/resource.go index 2c46a270..fc4400aa 100644 --- a/stackit/services/ske/project/resource.go +++ b/stackit/services/ske/project/resource.go @@ -56,7 +56,7 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR providerData, ok := req.ProviderData.(core.ProviderData) if !ok { - resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected stackit.ProviderData, got %T. Please report this issue to the provider developers.", req.ProviderData)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData)) return } @@ -75,12 +75,12 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR } if err != nil { - resp.Diagnostics.AddError("Could not Configure API Client", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v", err)) return } - tflog.Info(ctx, "SKE project client configured") r.client = apiClient + tflog.Info(ctx, "SKE project client configured") } // Schema returns the Terraform schema structure @@ -119,19 +119,19 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest projectId := model.ProjectId.ValueString() _, err := r.client.CreateProject(ctx, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("failed during SKE project creation", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Calling API: %v", err)) return } model.Id = types.StringValue(projectId) wr, err := ske.CreateProjectWaitHandler(ctx, r.client, projectId).SetTimeout(5 * time.Minute).WaitWithContext(ctx) if err != nil { - resp.Diagnostics.AddError("Error creating cluster", fmt.Sprintf("Project creation waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Project creation waiting: %v", err)) return } - got, ok := wr.(*ske.ProjectResponse) + _, ok := wr.(*ske.ProjectResponse) if !ok { - resp.Diagnostics.AddError("Error creating cluster", fmt.Sprintf("Wait result conversion, got %+v", got)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Wait result conversion, got %+v", wr)) return } diags := resp.State.Set(ctx, model) @@ -139,7 +139,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "SKE project created or updated") + tflog.Info(ctx, "SKE project created") } // Read refreshes the Terraform state with the latest data. @@ -151,23 +151,25 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re return } projectId := model.ProjectId.ValueString() - // read _, err := r.client.GetProject(ctx, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("failed during SKE project read", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) return } model.Id = types.StringValue(projectId) model.ProjectId = types.StringValue(projectId) diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } tflog.Info(ctx, "SKE project read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *projectResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *projectResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform // Update shouldn't be called - resp.Diagnostics.AddError("Error updating ", "project can't be updated") + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", "Project can't be updated") } // Delete deletes the resource and removes the Terraform state on success. @@ -183,12 +185,12 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest c := r.client _, err := c.DeleteProject(ctx, projectId).Execute() if err != nil { - resp.Diagnostics.AddError("failed deleting project", err.Error()) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) return } _, err = ske.DeleteProjectWaitHandler(ctx, r.client, projectId).SetTimeout(10 * time.Minute).WaitWithContext(ctx) if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Project deletion waiting: %v", err)) + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Project deletion waiting: %v", err)) return } tflog.Info(ctx, "SKE project deleted") @@ -199,8 +201,8 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // nolint:gocritic // function signature required by Terraform idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 1 || idParts[0] == "" { - resp.Diagnostics.AddError( - "Unexpected Import Identifier", + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing project", fmt.Sprintf("Expected import identifier with format: [project_id] Got: %q", req.ID), ) return diff --git a/stackit/validate/validate.go b/stackit/validate/validate.go index 7aab712a..8beaaea5 100644 --- a/stackit/validate/validate.go +++ b/stackit/validate/validate.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/stackitcloud/terraform-provider-stackit/stackit/core" ) @@ -38,47 +39,71 @@ func (v *Validator) ValidateString(ctx context.Context, req validator.StringRequ } func UUID() *Validator { + description := "value must be an UUID" + return &Validator{ - description: "validate string is UUID", + description: description, validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if _, err := uuid.Parse(req.ConfigValue.ValueString()); err != nil { - resp.Diagnostics.AddError("not a valid UUID", err.Error()) + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + description, + req.ConfigValue.ValueString(), + )) } }, } } func IP() *Validator { + description := "value must be an IP address" + return &Validator{ - description: "validate string is IP address", + description: description, validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if net.ParseIP(req.ConfigValue.ValueString()) == nil { - resp.Diagnostics.AddError("not a valid IP address", "") + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + description, + req.ConfigValue.ValueString(), + )) } }, } } func NoSeparator() *Validator { + description := fmt.Sprintf("value must not contain identifier separator '%s'", core.Separator) + return &Validator{ - description: "validate string does not contain internal separator", + description: description, validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if strings.Contains(req.ConfigValue.ValueString(), core.Separator) { - resp.Diagnostics.AddError("Invalid character found.", fmt.Sprintf("The string should not contain a '%s'", core.Separator)) + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + description, + req.ConfigValue.ValueString(), + )) } }, } } -func SemanticMinorVersion() *Validator { +func MinorVersionNumber() *Validator { + description := "value must be a minor version number, without a leading 'v': '[MAJOR].[MINOR]'" + return &Validator{ - description: "validate string does not contain internal separator", + description: description, validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { exp := `^\d+\.\d+?$` r := regexp.MustCompile(exp) version := req.ConfigValue.ValueString() if !r.MatchString(version) { - resp.Diagnostics.AddError("Invalid version.", "The version should be a valid semantic version only containing major and minor version. The version should not contain a leading `v`. Got "+version) + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + req.Path, + description, + req.ConfigValue.ValueString(), + )) } }, } diff --git a/stackit/validate/validate_test.go b/stackit/validate/validate_test.go index 39bdfaf9..9ea438b3 100644 --- a/stackit/validate/validate_test.go +++ b/stackit/validate/validate_test.go @@ -145,7 +145,7 @@ func TestNoSeparator(t *testing.T) { } } -func TestSemanticMinorVersion(t *testing.T) { +func TestMinorVersionNumber(t *testing.T) { tests := []struct { description string input string @@ -195,7 +195,7 @@ func TestSemanticMinorVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { r := validator.StringResponse{} - SemanticMinorVersion().ValidateString(context.Background(), validator.StringRequest{ + MinorVersionNumber().ValidateString(context.Background(), validator.StringRequest{ ConfigValue: types.StringValue(tt.input), }, &r)