From b29878c2808a1bd5477db795129550db5e6623d1 Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Mon, 8 Dec 2025 00:05:21 +0100 Subject: [PATCH 01/21] Resolve conflicts # Conflicts: # go.mod # go.sum # stackit/internal/testutil/testutil.go # stackit/provider.go # Conflicts: # stackit/internal/testutil/testutil.go --- docs/index.md | 1 + docs/resources/intake_runner.md | 34 ++ stackit/internal/core/core.go | 1 + .../services/intake/runner/resource.go | 529 ++++++++++++++++++ .../intake/runner/resource_acc_test.go | 160 ++++++ .../services/intake/runner/resource_test.go | 254 +++++++++ .../internal/services/intake/utils/utils.go | 31 + stackit/internal/testutil/testutil.go | 3 +- stackit/provider.go | 8 + 9 files changed, 1020 insertions(+), 1 deletion(-) create mode 100644 docs/resources/intake_runner.md create mode 100644 stackit/internal/services/intake/runner/resource.go create mode 100644 stackit/internal/services/intake/runner/resource_acc_test.go create mode 100644 stackit/internal/services/intake/runner/resource_test.go create mode 100644 stackit/internal/services/intake/utils/utils.go diff --git a/docs/index.md b/docs/index.md index 8c93ec933..31a78c16c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -163,6 +163,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: iam, routing-tables, network - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service +- `intake_custom_endpoint` (String) - `kms_custom_endpoint` (String) Custom endpoint for the KMS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service - `logme_custom_endpoint` (String) Custom endpoint for the LogMe service diff --git a/docs/resources/intake_runner.md b/docs/resources/intake_runner.md new file mode 100644 index 000000000..65a5c3206 --- /dev/null +++ b/docs/resources/intake_runner.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_intake_runner Resource - stackit" +subcategory: "" +description: |- + Manages STACKIT Intake Runner. +--- + +# stackit_intake_runner (Resource) + +Manages STACKIT Intake Runner. + + + + +## Schema + +### Required + +- `max_message_size_kib` (Number) The maximum message size in KiB. +- `max_messages_per_hour` (Number) The maximum number of messages per hour. +- `name` (String) The name of the runner. +- `project_id` (String) STACKIT Project ID to which the runner is associated. + +### Optional + +- `description` (String) The description of the runner. +- `labels` (Map of String) User-defined labels. +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`runner_id`". +- `runner_id` (String) The runner ID. diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 9817e6fd5..51f001251 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -49,6 +49,7 @@ type ProviderData struct { EdgeCloudCustomEndpoint string GitCustomEndpoint string IaaSCustomEndpoint string + IntakeCustomEndpoint string KMSCustomEndpoint string LoadBalancerCustomEndpoint string LogMeCustomEndpoint string diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go new file mode 100644 index 000000000..d9de6667a --- /dev/null +++ b/stackit/internal/services/intake/runner/resource.go @@ -0,0 +1,529 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "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-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + intakeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/stackit-sdk-go/services/intake/wait" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &runnerResource{} + _ resource.ResourceWithConfigure = &runnerResource{} + _ resource.ResourceWithImportState = &runnerResource{} + _ resource.ResourceWithModifyPlan = &runnerResource{} +) + +// Model is the internal model of the terraform resource +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + RunnerId types.String `tfsdk:"runner_id"` + Region types.String `tfsdk:"region"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Labels types.Map `tfsdk:"labels"` + MaxMessageSizeKiB types.Int64 `tfsdk:"max_message_size_kib"` + MaxMessagesPerHour types.Int64 `tfsdk:"max_messages_per_hour"` +} + +// NewRunnerResource is a helper function to simplify the provider implementation. +func NewRunnerResource() resource.Resource { + return &runnerResource{} +} + +// runnerResource is the resource implementation. +type runnerResource struct { + client *intake.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (r *runnerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_intake_runner" +} + +// Configure adds the provider configured client to the resource. +func (r *runnerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Intake runner client configured") +} + +// ModifyPlan implements resource.ResourceWithModifyPlan. +// Use the modifier to set the effective region in the current plan. +func (r *runnerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Schema defines the schema for the resource. +func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Manages STACKIT Intake Runner.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`runner_id`\".", + "project_id": "STACKIT Project ID to which the runner is associated.", + "runner_id": "The runner ID.", + "name": "The name of the runner.", + "region": "The resource region. If not defined, the provider region is used.", + "description": "The description of the runner.", + "labels": "User-defined labels.", + "max_message_size_kib": "The maximum message size in KiB.", + "max_messages_per_hour": "The maximum number of messages per hour.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "runner_id": schema.StringAttribute{ + Description: descriptions["runner_id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "max_message_size_kib": schema.Int64Attribute{ + Description: descriptions["max_message_size_kib"], + Required: true, + }, + "max_messages_per_hour": schema.Int64Attribute{ + Description: descriptions["max_messages_per_hour"], + Required: true, + }, + "region": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: descriptions["region"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("eu01"), // Currently Intake supports only EU01 region + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *runnerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + + // prepare the payload struct for the create bar request + payload, err := toCreatePayload(&model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Create new bar + runnerResp, err := r.client.CreateIntakeRunner(ctx, projectId, region).CreateIntakeRunnerPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Calling API: %v", err)) + return + } + ctx = core.LogResponse(ctx) + + // Wait for creation of intake runner + _, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, r.client, projectId, region, runnerResp.GetId()).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Intake runner creation waiting: %v", err)) + return + } + + err = mapFields(runnerResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Processing API payload: %v", err)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake runner created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *runnerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + runnerId := model.RunnerId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "runner_id", runnerId) + + runnerResp, err := r.client.GetIntakeRunner(ctx, projectId, region, runnerId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFields(runnerResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake runner read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *runnerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model, state Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + runnerId := model.RunnerId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "runner_id", runnerId) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toUpdatePayload(&model, &state) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + // Update runner + runnerResp, err := r.client.UpdateIntakeRunner(ctx, projectId, region, runnerId).UpdateIntakeRunnerPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Wait for update + _, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, r.client, projectId, region, runnerId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Runner update waiting: %v", err)) + return + } + + // Map response body to schema + err = mapFields(runnerResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Processing API response: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake runner updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *runnerResource) 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...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + runnerId := model.RunnerId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "runner_id", runnerId) + + // Delete existing bar + err := r.client.DeleteIntakeRunner(ctx, projectId, region, runnerId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "Intake runner already deleted") + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting runner", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Wait for the delete operation to complete + _, err = wait.DeleteIntakeRunnerWaitHandler(ctx, r.client, projectId, region, runnerId).WaitWithContext(ctx) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting runner", fmt.Sprintf("Runner deletion waiting: %v", err)) + return + } + + tflog.Info(ctx, "Intake runner deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the Intake runner resource import identifier is: [project_id],[region],[runner_id] +func (r *runnerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing intake runner", + fmt.Sprintf("Expected import identifier with format [project_id],[region],[runner_id], got %q", req.ID), + ) + return + } + + utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "runner_id": idParts[2], + }) + + tflog.Info(ctx, "Intake runner state imported") +} + +// Maps runner fields to the provider internal model +func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model) error { + if runnerResp == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var runnerId string + if runnerResp.Id != nil { + runnerId = *runnerResp.Id + } + + model.Id = utils.BuildInternalTerraformId( + model.ProjectId.ValueString(), + model.Region.ValueString(), + runnerId, + ) + + if runnerResp.Labels == nil { + model.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{}) + } else { + labels, diags := types.MapValueFrom(context.Background(), types.StringType, runnerResp.Labels) + if diags.HasError() { + return fmt.Errorf("converting labels: %w", core.DiagsToError(diags)) + } + model.Labels = labels + } + + if runnerResp.Id != nil && *runnerResp.Id == "" { + model.RunnerId = types.StringNull() + } else { + model.RunnerId = types.StringPointerValue(runnerResp.Id) + } + model.Name = types.StringPointerValue(runnerResp.DisplayName) + if runnerResp.Description == nil { + model.Description = types.StringValue("") + } else { + model.Description = types.StringPointerValue(runnerResp.Description) + } + model.MaxMessageSizeKiB = types.Int64PointerValue(runnerResp.MaxMessageSizeKiB) + model.MaxMessagesPerHour = types.Int64PointerValue(runnerResp.MaxMessagesPerHour) + return nil +} + +// Build CreateBarPayload from provider's model +func toCreatePayload(model *Model) (*intake.CreateIntakeRunnerPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + var labels map[string]string + if !model.Labels.IsNull() && !model.Labels.IsUnknown() { + diags := model.Labels.ElementsAs(context.Background(), &labels, false) + if diags.HasError() { + return nil, fmt.Errorf("converting labels: %w", core.DiagsToError(diags)) + } + } + + var labelsPtr *map[string]string + if len(labels) > 0 { + labelsPtr = &labels + } + + return &intake.CreateIntakeRunnerPayload{ + Description: conversion.StringValueToPointer(model.Description), + DisplayName: conversion.StringValueToPointer(model.Name), + Labels: labelsPtr, + MaxMessageSizeKiB: conversion.Int64ValueToPointer(model.MaxMessageSizeKiB), + MaxMessagesPerHour: conversion.Int64ValueToPointer(model.MaxMessagesPerHour), + }, nil +} + +func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, error) { + if model == nil { + return nil, fmt.Errorf("model is nil") + } + if state == nil { + return nil, fmt.Errorf("state is nil") + } + + payload := &intake.UpdateIntakeRunnerPayload{} + + if !model.Name.IsUnknown() { + payload.DisplayName = conversion.StringValueToPointer(model.Name) + } + + if !model.MaxMessageSizeKiB.IsUnknown() { + payload.MaxMessageSizeKiB = conversion.Int64ValueToPointer(model.MaxMessageSizeKiB) + } + + if !model.MaxMessagesPerHour.IsUnknown() { + payload.MaxMessagesPerHour = conversion.Int64ValueToPointer(model.MaxMessagesPerHour) + } + + // Handle optional fields + if !model.Description.IsUnknown() || model.Description.IsNull() { + if model.Description.IsNull() { + payload.Description = sdkUtils.Ptr("") + } else { + payload.Description = conversion.StringValueToPointer(model.Description) + } + } + + var labels map[string]string + if !model.Labels.IsUnknown() { + if model.Labels.IsNull() { + labels = map[string]string{} + payload.Labels = &labels + } else { + diags := model.Labels.ElementsAs(context.Background(), &labels, false) + if diags.HasError() { + return nil, fmt.Errorf("failed to convert labels: %w", core.DiagsToError(diags)) + } + payload.Labels = &labels + } + } + + return payload, nil +} diff --git a/stackit/internal/services/intake/runner/resource_acc_test.go b/stackit/internal/services/intake/runner/resource_acc_test.go new file mode 100644 index 000000000..7a65bc10a --- /dev/null +++ b/stackit/internal/services/intake/runner/resource_acc_test.go @@ -0,0 +1,160 @@ +package runner_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +// intakeRunnerResource is the name of the test resource +const intakeRunnerResource = "stackit_intake_runner.example" + +func TestAccIntakeRunner(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIntakeRunnerDestroy, + Steps: []resource.TestStep{ + // create the runner + { + Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigMinimal("example-runner-minimal"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-minimal"), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", "eu01"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), + ), + }, + // update the runner + { + Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigFull("example-runner-full", "An example runner for Intake", 1024, 1100), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-full"), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), + ), + }, + // importing the runner + { + ResourceName: intakeRunnerResource, + ImportState: true, + ImportStateVerify: true, + }, + // update to remove optional attributes + { + Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigUpdated("example-runner-updated", 1024, 1100), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-updated"), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), + ), + }, + }, + }) +} + +func testAccIntakeRunnerConfigMinimal(name string) string { + return fmt.Sprintf(` + resource "stackit_intake_runner" "example" { + project_id = "%s" + name = "%s" + region = "eu01" + max_message_size_kib = 1024 + max_messages_per_hour = 1000 + } + `, + testutil.ProjectId, + name, + ) +} + +func testAccIntakeRunnerConfigFull(name, description string, maxKib, maxPerHour int) string { + return fmt.Sprintf(` + resource "stackit_intake_runner" "example" { + project_id = "%s" + name = "%s" + description = "%s" + max_message_size_kib = %d + max_messages_per_hour = %d + labels = { + "created_by" = "terraform-provider-stackit" + "env" = "development" + } + region = "eu01" + } + `, + testutil.ProjectId, + name, + description, + maxKib, + maxPerHour, + ) +} + +func testAccIntakeRunnerConfigUpdated(name string, maxKib, maxPerHour int) string { + return fmt.Sprintf(` + resource "stackit_intake_runner" "example" { + project_id = "%s" + name = "%s" + description = "" + max_message_size_kib = %d + max_messages_per_hour = %d + labels = {} + region = "eu01" + } + `, + testutil.ProjectId, + name, + maxKib, + maxPerHour, + ) +} + +func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { + ctx := context.Background() + var client *intake.APIClient + var err error + if testutil.IntakeCustomEndpoint == "" { + client, err = intake.NewAPIClient( + sdkConfig.WithRegion("eu01"), + ) + } else { + client, err = intake.NewAPIClient(sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint)) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_intake_runner" { + continue + } + // Try to find the runner + _, err := client.GetIntakeRunner(ctx, rs.Primary.Attributes["project_id"], rs.Primary.Attributes["region"], rs.Primary.Attributes["runner_id"]).Execute() + if err == nil { + return fmt.Errorf("intake runner with ID %s still exists", rs.Primary.ID) + } + var oapiErr *oapierror.GenericOpenAPIError + if !errors.As(err, &oapiErr) || oapiErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("expected 404 not found, got error: %w", err) + } + } + + return nil +} diff --git a/stackit/internal/services/intake/runner/resource_test.go b/stackit/internal/services/intake/runner/resource_test.go new file mode 100644 index 000000000..921349c64 --- /dev/null +++ b/stackit/internal/services/intake/runner/resource_test.go @@ -0,0 +1,254 @@ +package runner + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" +) + +func TestMapFields(t *testing.T) { + runnerId := uuid.New().String() + tests := []struct { + description string + input *intake.IntakeRunnerResponse + model *Model + expected *Model + wantErr bool + }{ + { + "success", + &intake.IntakeRunnerResponse{ + Id: utils.Ptr(runnerId), + DisplayName: utils.Ptr("name"), + Description: utils.Ptr("description"), + Labels: &map[string]string{"key": "value"}, + MaxMessageSizeKiB: utils.Ptr(int64(1024)), + MaxMessagesPerHour: utils.Ptr(int64(100)), + }, + &Model{ + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + }, + &Model{ + Id: types.StringValue(fmt.Sprintf("pid,eu01,%s", runnerId)), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + RunnerId: types.StringValue(runnerId), + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + MaxMessageSizeKiB: types.Int64Value(1024), + MaxMessagesPerHour: types.Int64Value(100), + }, + false, + }, + { + "nil input", + nil, + &Model{}, + nil, + true, + }, + { + "nil model", + &intake.IntakeRunnerResponse{}, + nil, + nil, + true, + }, + { + "empty response", + &intake.IntakeRunnerResponse{ + Id: utils.Ptr(""), + Labels: &map[string]string{}, + }, + &Model{ + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + }, + &Model{ + Id: types.StringValue("pid,eu01,"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue("eu01"), + RunnerId: types.StringNull(), + Name: types.StringNull(), + Description: types.StringValue(""), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), + MaxMessageSizeKiB: types.Int64Null(), + MaxMessagesPerHour: types.Int64Null(), + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(tt.input, tt.model) + if (err != nil) != tt.wantErr { + t.Errorf("mapFields error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, tt.model); diff != "" { + t.Errorf("mapFields mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + model *Model + expected *intake.CreateIntakeRunnerPayload + wantErr bool + }{ + { + "success", + &Model{ + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + MaxMessageSizeKiB: types.Int64Value(1024), + MaxMessagesPerHour: types.Int64Value(100), + }, + &intake.CreateIntakeRunnerPayload{ + DisplayName: utils.Ptr("name"), + Description: utils.Ptr("description"), + Labels: utils.Ptr(map[string]string{"key": "value"}), + MaxMessageSizeKiB: utils.Ptr(int64(1024)), + MaxMessagesPerHour: utils.Ptr(int64(100)), + }, + false, + }, + { + "nil model", + nil, + nil, + true, + }, + { + "empty model", + &Model{}, + &intake.CreateIntakeRunnerPayload{ + DisplayName: nil, + Description: nil, + Labels: nil, + MaxMessageSizeKiB: nil, + MaxMessagesPerHour: nil, + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + payload, err := toCreatePayload(tt.model) + if (err != nil) != tt.wantErr { + t.Errorf("toCreatePayload error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, payload); diff != "" { + t.Errorf("toCreatePayload mismatch (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + model *Model + state *Model + expected *intake.UpdateIntakeRunnerPayload + wantErr bool + }{ + { + "success", + &Model{ + Name: types.StringValue("name"), + Description: types.StringValue("description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + MaxMessageSizeKiB: types.Int64Value(1024), + MaxMessagesPerHour: types.Int64Value(100), + }, + &Model{}, + &intake.UpdateIntakeRunnerPayload{ + DisplayName: conversion.StringValueToPointer(types.StringValue("name")), + Description: conversion.StringValueToPointer(types.StringValue("description")), + Labels: utils.Ptr(map[string]string{"key": "value"}), + MaxMessageSizeKiB: conversion.Int64ValueToPointer(types.Int64Value(1024)), + MaxMessagesPerHour: conversion.Int64ValueToPointer(types.Int64Value(100)), + }, + false, + }, + { + "nil model", + nil, + &Model{}, + nil, + true, + }, + { + "nil state", + &Model{}, + nil, + nil, + true, + }, + { + "empty model", + &Model{}, + &Model{}, + &intake.UpdateIntakeRunnerPayload{ + Description: utils.Ptr(""), + Labels: &map[string]string{}, + }, + false, + }, + { + "unknown values", + &Model{ + Name: types.StringUnknown(), + Description: types.StringUnknown(), + Labels: types.MapUnknown(types.StringType), + MaxMessageSizeKiB: types.Int64Unknown(), + MaxMessagesPerHour: types.Int64Unknown(), + }, + &Model{}, + &intake.UpdateIntakeRunnerPayload{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var labels map[string]string + if tt.model != nil && !tt.model.Labels.IsNull() && !tt.model.Labels.IsUnknown() { + diags := tt.model.Labels.ElementsAs(context.Background(), &labels, false) + if diags.HasError() { + t.Fatalf("error preparing test %v", diags) + } + } + + payload, err := toUpdatePayload(tt.model, tt.state) + if (err != nil) != tt.wantErr { + t.Errorf("toUpdatePayload error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if diff := cmp.Diff(tt.expected, payload); diff != "" { + t.Errorf("toUpdatePayload mismatch (-want +got):\n%s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/intake/utils/utils.go b/stackit/internal/services/intake/utils/utils.go new file mode 100644 index 000000000..b6357b496 --- /dev/null +++ b/stackit/internal/services/intake/utils/utils.go @@ -0,0 +1,31 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *intake.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.IntakeCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IntakeCustomEndpoint)) + } else { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) + } + apiClient, err := intake.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 38cd9cd36..4d99c2cd0 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -91,8 +91,9 @@ var ( ServerUpdateCustomEndpoint = os.Getenv("TF_ACC_SERVER_UPDATE_CUSTOM_ENDPOINT") SFSCustomEndpoint = os.Getenv("TF_ACC_SFS_CUSTOM_ENDPOINT") ServiceAccountCustomEndpoint = os.Getenv("TF_ACC_SERVICE_ACCOUNT_CUSTOM_ENDPOINT") - TokenCustomEndpoint = os.Getenv("TF_ACC_TOKEN_CUSTOM_ENDPOINT") SKECustomEndpoint = os.Getenv("TF_ACC_SKE_CUSTOM_ENDPOINT") + IntakeCustomEndpoint = os.Getenv("TF_ACC_INTAKE_CUSTOM_ENDPOINT") + TokenCustomEndpoint = os.Getenv("TF_ACC_TOKEN_CUSTOM_ENDPOINT") ) // Provider config helper functions diff --git a/stackit/provider.go b/stackit/provider.go index 99c6c38ea..ba8b4532a 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -57,6 +57,7 @@ import ( iaasServiceAccountAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/serviceaccountattach" iaasVolume "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volume" iaasVolumeAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volumeattach" + intakeRunner "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/runner" kmsKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key" kmsKeyRing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/keyring" kmsWrappingKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/wrapping-key" @@ -153,6 +154,7 @@ type providerModel struct { EdgeCloudCustomEndpoint types.String `tfsdk:"edgecloud_custom_endpoint"` GitCustomEndpoint types.String `tfsdk:"git_custom_endpoint"` IaaSCustomEndpoint types.String `tfsdk:"iaas_custom_endpoint"` + IntakeCustomEndpoint types.String `tfsdk:"intake_custom_endpoint"` KmsCustomEndpoint types.String `tfsdk:"kms_custom_endpoint"` LoadBalancerCustomEndpoint types.String `tfsdk:"loadbalancer_custom_endpoint"` LogMeCustomEndpoint types.String `tfsdk:"logme_custom_endpoint"` @@ -307,6 +309,10 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro Optional: true, Description: descriptions["iaas_custom_endpoint"], }, + "intake_custom_endpoint": schema.StringAttribute{ + Optional: true, + Description: descriptions["intake_custom_endpoint"], + }, "kms_custom_endpoint": schema.StringAttribute{ Optional: true, Description: descriptions["kms_custom_endpoint"], @@ -462,6 +468,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.EdgeCloudCustomEndpoint, func(v string) { providerData.EdgeCloudCustomEndpoint = v }) setStringField(providerConfig.GitCustomEndpoint, func(v string) { providerData.GitCustomEndpoint = v }) setStringField(providerConfig.IaaSCustomEndpoint, func(v string) { providerData.IaaSCustomEndpoint = v }) + setStringField(providerConfig.IntakeCustomEndpoint, func(v string) { providerData.IntakeCustomEndpoint = v }) setStringField(providerConfig.KmsCustomEndpoint, func(v string) { providerData.KMSCustomEndpoint = v }) setStringField(providerConfig.LoadBalancerCustomEndpoint, func(v string) { providerData.LoadBalancerCustomEndpoint = v }) setStringField(providerConfig.LogMeCustomEndpoint, func(v string) { providerData.LogMeCustomEndpoint = v }) @@ -635,6 +642,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasSecurityGroupRule.NewSecurityGroupRuleResource, iaasRoutingTable.NewRoutingTableResource, iaasRoutingTableRoute.NewRoutingTableRouteResource, + intakeRunner.NewRunnerResource, kmsKey.NewKeyResource, kmsKeyRing.NewKeyRingResource, kmsWrappingKey.NewWrappingKeyResource, From ff34447a067923a81c367c60413d3b934116824e Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Mon, 8 Dec 2025 09:07:58 +0100 Subject: [PATCH 02/21] added example of intake runner --- .../resources/stackit_intake_runner/resource.tf | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/resources/stackit_intake_runner/resource.tf diff --git a/examples/resources/stackit_intake_runner/resource.tf b/examples/resources/stackit_intake_runner/resource.tf new file mode 100644 index 000000000..ceda583ff --- /dev/null +++ b/examples/resources/stackit_intake_runner/resource.tf @@ -0,0 +1,17 @@ +resource "stackit_intake_runner" "example" { + project_id = var.project_id + name = "example-runner-full" + description = "An example runner for STACKIT Intake" + max_message_size_kib = 2048 + max_messages_per_hour = 1500 + labels = { + "created_by" = "terraform-example" + "env" = "production" + } + region = var.region +} + +import { + to = stackit_intake_runner.example + id = "${var.project_id},${var.region},${var.runner_id}" +} \ No newline at end of file From 4dc038b14882159ae3c825be1482eb75d9f7b4ef Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Tue, 16 Dec 2025 22:19:46 +0100 Subject: [PATCH 03/21] added datasource for intake runner --- .../services/intake/runner/data_source.go | 167 ++++++++++++++++++ .../services/intake/runner/resource.go | 2 +- stackit/provider.go | 1 + 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 stackit/internal/services/intake/runner/data_source.go diff --git a/stackit/internal/services/intake/runner/data_source.go b/stackit/internal/services/intake/runner/data_source.go new file mode 100644 index 000000000..84728b714 --- /dev/null +++ b/stackit/internal/services/intake/runner/data_source.go @@ -0,0 +1,167 @@ +package runner + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "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/oapierror" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + intakeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" + + "github.com/stackitcloud/stackit-sdk-go/services/intake" +) + +// Ensure the implementation satisfies the expected interfaces +var ( + _ datasource.DataSource = &runnerDataSource{} +) + +// NewRunnerDataSource is a helper function to simplify the provider implementation +func NewRunnerDataSource() datasource.DataSource { + return &runnerDataSource{} +} + +type runnerDataSource struct { + client *intake.APIClient +} + +func (r *runnerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_intake_runner" +} + +// Configure adds the provider configured client to the data source +func (r *runnerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "Intake runner client configured for data source") +} + +// Schema defines the schema for the data source +func (r *runnerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + descriptions := map[string]string{ + "main": "Datasource for STACKIT Intake Runner.", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`runner_id`\".", + "project_id": "STACKIT Project ID to which the runner is associated.", + "runner_id": "The runner ID.", + "name": "The name of the runner.", + "region": "The resource region.", + "description": "The description of the runner.", + "labels": "User-defined labels.", + "max_message_size_kib": "The maximum message size in KiB.", + "max_messages_per_hour": "The maximum number of messages per hour.", + } + + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "runner_id": schema.StringAttribute{ + Description: descriptions["runner_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Required: true, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Computed: true, + }, + "description": schema.StringAttribute{ + Description: descriptions["description"], + Computed: true, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + ElementType: types.StringType, + Computed: true, + }, + "max_message_size_kib": schema.Int64Attribute{ + Description: descriptions["max_message_size_kib"], + Computed: true, + }, + "max_messages_per_hour": schema.Int64Attribute{ + Description: descriptions["max_messages_per_hour"], + Computed: true, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *runnerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := model.Region.ValueString() + runnerId := model.RunnerId.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "runner_id", runnerId) + + runnerResp, err := r.client.GetIntakeRunner(ctx, projectId, region, runnerId).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Runner with ID %s not found in project %s and region %s", runnerId, projectId, region)) + return + } + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(runnerResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Intake runner read") +} diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index d9de6667a..8c7a6b93a 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -115,7 +115,7 @@ func (r *runnerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlan func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ "main": "Manages STACKIT Intake Runner.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`runner_id`\".", + "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`runner_id`\".", "project_id": "STACKIT Project ID to which the runner is associated.", "runner_id": "The runner ID.", "name": "The name of the runner.", diff --git a/stackit/provider.go b/stackit/provider.go index ba8b4532a..d5b073d61 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -559,6 +559,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasRoutingTables.NewRoutingTablesDataSource, iaasRoutingTableRoutes.NewRoutingTableRoutesDataSource, iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, + intakeRunner.NewRunnerDataSource, kmsKey.NewKeyDataSource, kmsKeyRing.NewKeyRingDataSource, kmsWrappingKey.NewWrappingKeyDataSource, From d9cd4332d13ced2bfdeaf658a30696133b48e88d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 11:24:22 +0100 Subject: [PATCH 04/21] Resolve conflicts # Conflicts: # go.sum --- go.mod | 1 + go.sum | 2 ++ stackit/internal/testutil/testutil.go | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/go.mod b/go.mod index b685ded3f..effc99783 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/edge v0.4.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.10.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.0 + github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.2 github.com/stackitcloud/stackit-sdk-go/services/kms v1.2.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.7.1 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.4 diff --git a/go.sum b/go.sum index deafe8de2..9583aae4d 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.10.1 h1:3JKXfI5hdcXcRVBjU github.com/stackitcloud/stackit-sdk-go/services/git v0.10.1/go.mod h1:3nTaj8IGjNNGYUD2CpuXkXwc5c4giTUmoPggFhjVFxo= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.0 h1:U/x0tc487X9msMS5yZYjrBAAKrCx87Trmt0kh8JiARA= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.0/go.mod h1:6+5+RCDfU7eQN3+/SGdOtx7Bq9dEa2FrHz/jflgY1M4= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.2 h1:xywfPSTBV6lqcnueI++KsyWvnZTKCfoCVp8/kzT/RXE= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.2/go.mod h1:Ki7BldvSi1f5Lhy7iDeBkAhUvgXPCSAsaqFuxrkPDpg= github.com/stackitcloud/stackit-sdk-go/services/kms v1.2.0 h1:Ar2n9GKmrTN80G/Ta1R+fL5aX5nEoxL6ODVJl3emzho= github.com/stackitcloud/stackit-sdk-go/services/kms v1.2.0/go.mod h1:sHMFoYvVrkRZcH13DkLvp48nW+ssRVVVuwqJHDGpa5M= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.7.1 h1:2UKKtBg+E5IFUcZAkRyKz1Km+tsA//zGjFQjVWwaXss= diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 4d99c2cd0..e20247a69 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -209,6 +209,20 @@ func IaaSProviderConfigWithExperiments() string { ) } +func IntakeProviderConfig() string { + if IntakeCustomEndpoint == "" { + return `provider "stackit" { + default_region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + intake_custom_endpoint = "%s" + }`, + IntakeCustomEndpoint, + ) +} + func KMSProviderConfig() string { if KMSCustomEndpoint == "" { return ` From fd4cf8733a133e82ee99f59036382cb9848d47ce Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 11:33:04 +0100 Subject: [PATCH 05/21] Add docs --- docs/data-sources/intake_runner.md | 31 ++++++++++++++++++++++++++++++ docs/resources/intake_runner.md | 24 +++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/data-sources/intake_runner.md diff --git a/docs/data-sources/intake_runner.md b/docs/data-sources/intake_runner.md new file mode 100644 index 000000000..d914e5d61 --- /dev/null +++ b/docs/data-sources/intake_runner.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_intake_runner Data Source - stackit" +subcategory: "" +description: |- + Datasource for STACKIT Intake Runner. +--- + +# stackit_intake_runner (Data Source) + +Datasource for STACKIT Intake Runner. + + + + +## Schema + +### Required + +- `project_id` (String) STACKIT Project ID to which the runner is associated. +- `region` (String) The resource region. +- `runner_id` (String) The runner ID. + +### Read-Only + +- `description` (String) The description of the runner. +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`runner_id`". +- `labels` (Map of String) User-defined labels. +- `max_message_size_kib` (Number) The maximum message size in KiB. +- `max_messages_per_hour` (Number) The maximum number of messages per hour. +- `name` (String) The name of the runner. diff --git a/docs/resources/intake_runner.md b/docs/resources/intake_runner.md index 65a5c3206..9a246d535 100644 --- a/docs/resources/intake_runner.md +++ b/docs/resources/intake_runner.md @@ -10,7 +10,27 @@ description: |- Manages STACKIT Intake Runner. - +## Example Usage + +```terraform +resource "stackit_intake_runner" "example" { + project_id = var.project_id + name = "example-runner-full" + description = "An example runner for STACKIT Intake" + max_message_size_kib = 2048 + max_messages_per_hour = 1500 + labels = { + "created_by" = "terraform-example" + "env" = "production" + } + region = var.region +} + +import { + to = stackit_intake_runner.example + id = "${var.project_id},${var.region},${var.runner_id}" +} +``` ## Schema @@ -30,5 +50,5 @@ Manages STACKIT Intake Runner. ### Read-Only -- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`runner_id`". +- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`region`,`runner_id`". - `runner_id` (String) The runner ID. From 03b24731d47ef358fc4562a01005a01af5a8f206 Mon Sep 17 00:00:00 2001 From: BipBopBipBop Date: Thu, 22 Jan 2026 11:56:50 +0100 Subject: [PATCH 06/21] Update stackit/internal/services/intake/runner/data_source.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruben Hönle --- stackit/internal/services/intake/runner/data_source.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/intake/runner/data_source.go b/stackit/internal/services/intake/runner/data_source.go index 84728b714..200fe0c9c 100644 --- a/stackit/internal/services/intake/runner/data_source.go +++ b/stackit/internal/services/intake/runner/data_source.go @@ -45,7 +45,7 @@ func (r *runnerDataSource) Configure(ctx context.Context, req datasource.Configu return } - apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + r.client := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } From 7bfdb2fa04d765d02fd031a4d91ac5d812e2170a Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:12:32 +0100 Subject: [PATCH 07/21] Remove region field --- docs/data-sources/intake_runner.md | 1 - docs/index.md | 2 +- .../services/intake/runner/data_source.go | 7 +------ .../services/intake/runner/resource_acc_test.go | 15 +++++++-------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/docs/data-sources/intake_runner.md b/docs/data-sources/intake_runner.md index d914e5d61..60950b2f1 100644 --- a/docs/data-sources/intake_runner.md +++ b/docs/data-sources/intake_runner.md @@ -18,7 +18,6 @@ Datasource for STACKIT Intake Runner. ### Required - `project_id` (String) STACKIT Project ID to which the runner is associated. -- `region` (String) The resource region. - `runner_id` (String) The runner ID. ### Read-Only diff --git a/docs/index.md b/docs/index.md index 31a78c16c..108fb183d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -163,7 +163,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: iam, routing-tables, network - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service -- `intake_custom_endpoint` (String) +- `intake_custom_endpoint` (String) Custom endpoint for the Intake service - `kms_custom_endpoint` (String) Custom endpoint for the KMS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service - `logme_custom_endpoint` (String) Custom endpoint for the LogMe service diff --git a/stackit/internal/services/intake/runner/data_source.go b/stackit/internal/services/intake/runner/data_source.go index 200fe0c9c..ffa1aa1d1 100644 --- a/stackit/internal/services/intake/runner/data_source.go +++ b/stackit/internal/services/intake/runner/data_source.go @@ -45,7 +45,7 @@ func (r *runnerDataSource) Configure(ctx context.Context, req datasource.Configu return } - r.client := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -61,7 +61,6 @@ func (r *runnerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, "project_id": "STACKIT Project ID to which the runner is associated.", "runner_id": "The runner ID.", "name": "The name of the runner.", - "region": "The resource region.", "description": "The description of the runner.", "labels": "User-defined labels.", "max_message_size_kib": "The maximum message size in KiB.", @@ -91,10 +90,6 @@ func (r *runnerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Required: true, - }, "name": schema.StringAttribute{ Description: descriptions["name"], Computed: true, diff --git a/stackit/internal/services/intake/runner/resource_acc_test.go b/stackit/internal/services/intake/runner/resource_acc_test.go index 7a65bc10a..948c879ad 100644 --- a/stackit/internal/services/intake/runner/resource_acc_test.go +++ b/stackit/internal/services/intake/runner/resource_acc_test.go @@ -29,7 +29,6 @@ func TestAccIntakeRunner(t *testing.T) { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-minimal"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", "eu01"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), @@ -74,7 +73,6 @@ func testAccIntakeRunnerConfigMinimal(name string) string { resource "stackit_intake_runner" "example" { project_id = "%s" name = "%s" - region = "eu01" max_message_size_kib = 1024 max_messages_per_hour = 1000 } @@ -96,7 +94,6 @@ func testAccIntakeRunnerConfigFull(name, description string, maxKib, maxPerHour "created_by" = "terraform-provider-stackit" "env" = "development" } - region = "eu01" } `, testutil.ProjectId, @@ -116,7 +113,6 @@ func testAccIntakeRunnerConfigUpdated(name string, maxKib, maxPerHour int) strin max_message_size_kib = %d max_messages_per_hour = %d labels = {} - region = "eu01" } `, testutil.ProjectId, @@ -131,9 +127,7 @@ func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { var client *intake.APIClient var err error if testutil.IntakeCustomEndpoint == "" { - client, err = intake.NewAPIClient( - sdkConfig.WithRegion("eu01"), - ) + client, err = intake.NewAPIClient() } else { client, err = intake.NewAPIClient(sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint)) } @@ -148,7 +142,12 @@ func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { // Try to find the runner _, err := client.GetIntakeRunner(ctx, rs.Primary.Attributes["project_id"], rs.Primary.Attributes["region"], rs.Primary.Attributes["runner_id"]).Execute() if err == nil { - return fmt.Errorf("intake runner with ID %s still exists", rs.Primary.ID) + err = client.DeleteIntakeRunner(ctx, rs.Primary.Attributes["project_id"], rs.Primary.Attributes["region"], rs.Primary.Attributes["runner_id"]).Execute() + if err != nil { + return fmt.Errorf("intake runner with ID %s still existed, got an error removing", rs.Primary.ID, err) + } + + return fmt.Errorf("intake runner with ID %s still existed", rs.Primary.ID) } var oapiErr *oapierror.GenericOpenAPIError if !errors.As(err, &oapiErr) || oapiErr.StatusCode != http.StatusNotFound { From ad76b338af796f8398189927a29d7224329acc6a Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:12:50 +0100 Subject: [PATCH 08/21] Add description to custom endpoint --- stackit/provider.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/provider.go b/stackit/provider.go index d5b073d61..d8c6e8aca 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -201,6 +201,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "edgecloud_custom_endpoint": "Custom endpoint for the Edge Cloud service", "git_custom_endpoint": "Custom endpoint for the Git service", "iaas_custom_endpoint": "Custom endpoint for the IaaS service", + "intake_custom_endpoint": "Custom endpoint for the Intake service", "kms_custom_endpoint": "Custom endpoint for the KMS service", "mongodbflex_custom_endpoint": "Custom endpoint for the MongoDB Flex service", "modelserving_custom_endpoint": "Custom endpoint for the AI Model Serving service", From 6d2c322b649b70330cf77f2f597a025f37374099 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:47:36 +0100 Subject: [PATCH 09/21] Adjust fields for resource.go (still missing region) --- .../services/intake/runner/resource.go | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index 8c7a6b93a..4d2a208b1 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -489,18 +489,8 @@ func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, er } payload := &intake.UpdateIntakeRunnerPayload{} - - if !model.Name.IsUnknown() { - payload.DisplayName = conversion.StringValueToPointer(model.Name) - } - - if !model.MaxMessageSizeKiB.IsUnknown() { - payload.MaxMessageSizeKiB = conversion.Int64ValueToPointer(model.MaxMessageSizeKiB) - } - - if !model.MaxMessagesPerHour.IsUnknown() { - payload.MaxMessagesPerHour = conversion.Int64ValueToPointer(model.MaxMessagesPerHour) - } + payload.MaxMessageSizeKiB = conversion.Int64ValueToPointer(model.MaxMessageSizeKiB) + payload.MaxMessagesPerHour = conversion.Int64ValueToPointer(model.MaxMessagesPerHour) // Handle optional fields if !model.Description.IsUnknown() || model.Description.IsNull() { @@ -513,10 +503,7 @@ func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, er var labels map[string]string if !model.Labels.IsUnknown() { - if model.Labels.IsNull() { - labels = map[string]string{} - payload.Labels = &labels - } else { + if !model.Labels.IsNull() { diags := model.Labels.ElementsAs(context.Background(), &labels, false) if diags.HasError() { return nil, fmt.Errorf("failed to convert labels: %w", core.DiagsToError(diags)) From e72e202d0586687140e6bcc512b169b3b63af8c4 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 13:33:43 +0100 Subject: [PATCH 10/21] Reintroduce region as optional field in data resource --- .../services/intake/runner/data_source.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/stackit/internal/services/intake/runner/data_source.go b/stackit/internal/services/intake/runner/data_source.go index ffa1aa1d1..f44dfaf69 100644 --- a/stackit/internal/services/intake/runner/data_source.go +++ b/stackit/internal/services/intake/runner/data_source.go @@ -31,7 +31,8 @@ func NewRunnerDataSource() datasource.DataSource { } type runnerDataSource struct { - client *intake.APIClient + client *intake.APIClient + providerData core.ProviderData } func (r *runnerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -40,12 +41,13 @@ func (r *runnerDataSource) Metadata(_ context.Context, req datasource.MetadataRe // Configure adds the provider configured client to the data source func (r *runnerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } - apiClient := intakeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + apiClient := intakeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -65,6 +67,7 @@ func (r *runnerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, "labels": "User-defined labels.", "max_message_size_kib": "The maximum message size in KiB.", "max_messages_per_hour": "The maximum number of messages per hour.", + "region": "The resource region. If not defined, the provider region is used.", } resp.Schema = schema.Schema{ @@ -111,6 +114,10 @@ func (r *runnerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, Description: descriptions["max_messages_per_hour"], Computed: true, }, + "region": schema.StringAttribute{ + Optional: true, + Description: descriptions["region"], + }, }, } } @@ -126,7 +133,7 @@ func (r *runnerDataSource) Read(ctx context.Context, req datasource.ReadRequest, ctx = core.InitProviderContext(ctx) projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) runnerId := model.RunnerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) @@ -147,7 +154,7 @@ func (r *runnerDataSource) Read(ctx context.Context, req datasource.ReadRequest, ctx = core.LogResponse(ctx) - err = mapFields(runnerResp, &model) + err = mapFields(runnerResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Processing API payload: %v", err)) return From 9f108daa93b9d27731f56a5e0639282a3fa997df Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 13:51:35 +0100 Subject: [PATCH 11/21] Adjust resource implementation and implement review comments --- .../services/intake/runner/resource.go | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index 4d2a208b1..40b02599b 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -18,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" intakeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/intake/utils" @@ -42,12 +40,12 @@ type Model struct { Id types.String `tfsdk:"id"` // needed by TF ProjectId types.String `tfsdk:"project_id"` RunnerId types.String `tfsdk:"runner_id"` - Region types.String `tfsdk:"region"` Name types.String `tfsdk:"name"` Description types.String `tfsdk:"description"` Labels types.Map `tfsdk:"labels"` MaxMessageSizeKiB types.Int64 `tfsdk:"max_message_size_kib"` MaxMessagesPerHour types.Int64 `tfsdk:"max_messages_per_hour"` + Region types.String `tfsdk:"region"` } // NewRunnerResource is a helper function to simplify the provider implementation. @@ -132,12 +130,16 @@ func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, res "id": schema.StringAttribute{ Description: descriptions["id"], Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, "project_id": schema.StringAttribute{ Description: descriptions["project_id"], Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, Validators: []validator.String{ validate.UUID(), @@ -154,6 +156,9 @@ func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, res "name": schema.StringAttribute{ Description: descriptions["name"], Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, "description": schema.StringAttribute{ Description: descriptions["description"], @@ -187,9 +192,6 @@ func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, res PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, - Validators: []validator.String{ - stringvalidator.OneOf("eu01"), // Currently Intake supports only EU01 region - }, }, }, } @@ -217,7 +219,7 @@ func (r *runnerResource) Create(ctx context.Context, req resource.CreateRequest, return } - // Create new bar + // Create new runner runnerResp, err := r.client.CreateIntakeRunner(ctx, projectId, region).CreateIntakeRunnerPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Calling API: %v", err)) @@ -232,7 +234,7 @@ func (r *runnerResource) Create(ctx context.Context, req resource.CreateRequest, return } - err = mapFields(runnerResp, &model) + err = mapFields(runnerResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating runner", fmt.Sprintf("Processing API payload: %v", err)) return @@ -277,7 +279,7 @@ func (r *runnerResource) Read(ctx context.Context, req resource.ReadRequest, res ctx = core.LogResponse(ctx) // Map response body to schema - err = mapFields(runnerResp, &model) + err = mapFields(runnerResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading runner", fmt.Sprintf("Processing API payload: %v", err)) return @@ -304,7 +306,7 @@ func (r *runnerResource) Update(ctx context.Context, req resource.UpdateRequest, projectId := model.ProjectId.ValueString() runnerId := model.RunnerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) + region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "runner_id", runnerId) ctx = tflog.SetField(ctx, "region", region) @@ -330,7 +332,7 @@ func (r *runnerResource) Update(ctx context.Context, req resource.UpdateRequest, } // Map response body to schema - err = mapFields(runnerResp, &model) + err = mapFields(runnerResp, &model, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating runner", fmt.Sprintf("Processing API response: %v", err)) return @@ -361,7 +363,7 @@ func (r *runnerResource) Delete(ctx context.Context, req resource.DeleteRequest, ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "runner_id", runnerId) - // Delete existing bar + // Delete existing runner err := r.client.DeleteIntakeRunner(ctx, projectId, region, runnerId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -407,7 +409,7 @@ func (r *runnerResource) ImportState(ctx context.Context, req resource.ImportSta } // Maps runner fields to the provider internal model -func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model) error { +func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model, region string) error { if runnerResp == nil { return fmt.Errorf("response input is nil") } @@ -422,7 +424,7 @@ func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model) error { model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), - model.Region.ValueString(), + region, runnerId, ) @@ -447,12 +449,13 @@ func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model) error { } else { model.Description = types.StringPointerValue(runnerResp.Description) } + model.Region = types.StringValue(region) model.MaxMessageSizeKiB = types.Int64PointerValue(runnerResp.MaxMessageSizeKiB) model.MaxMessagesPerHour = types.Int64PointerValue(runnerResp.MaxMessagesPerHour) return nil } -// Build CreateBarPayload from provider's model +// Build CreateIntakeRunnerPayload from provider's model func toCreatePayload(model *Model) (*intake.CreateIntakeRunnerPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") @@ -480,6 +483,7 @@ func toCreatePayload(model *Model) (*intake.CreateIntakeRunnerPayload, error) { }, nil } +// Build UpdateIntakeRunnerPayload from provider's model func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, error) { if model == nil { return nil, fmt.Errorf("model is nil") @@ -492,14 +496,9 @@ func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, er payload.MaxMessageSizeKiB = conversion.Int64ValueToPointer(model.MaxMessageSizeKiB) payload.MaxMessagesPerHour = conversion.Int64ValueToPointer(model.MaxMessagesPerHour) - // Handle optional fields - if !model.Description.IsUnknown() || model.Description.IsNull() { - if model.Description.IsNull() { - payload.Description = sdkUtils.Ptr("") - } else { - payload.Description = conversion.StringValueToPointer(model.Description) - } - } + // Optional fields + payload.DisplayName = conversion.StringValueToPointer(model.Name) + payload.Description = conversion.StringValueToPointer(model.Description) var labels map[string]string if !model.Labels.IsUnknown() { From d5bbdb5b3f374dfbeb2f575f48a0c48cd2fa3b79 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 14:45:43 +0100 Subject: [PATCH 12/21] Reshape resource_acc_test --- .../services/intake/resource_acc_test.go | 199 ++++++++++++++++++ .../services/intake/runner/resource.go | 12 +- .../intake/runner/resource_acc_test.go | 159 -------------- .../services/intake/testdata/resource-max.tf | 15 ++ .../services/intake/testdata/resource-min.tf | 10 + 5 files changed, 229 insertions(+), 166 deletions(-) create mode 100644 stackit/internal/services/intake/resource_acc_test.go delete mode 100644 stackit/internal/services/intake/runner/resource_acc_test.go create mode 100644 stackit/internal/services/intake/testdata/resource-max.tf create mode 100644 stackit/internal/services/intake/testdata/resource-min.tf diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go new file mode 100644 index 000000000..4c8cb9fd9 --- /dev/null +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -0,0 +1,199 @@ +package intake_test + +import ( + "context" + _ "embed" + "errors" + "fmt" + "maps" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +//go:embed testdata/resource-min.tf +var resourceMin string + +//go:embed testdata/resource-max.tf +var resourceMax string + +const intakeRunnerResource = "stackit_intake_runner.example" + +const ( + intakeRunnerMinName = "intake-min-runner" + intakeRunnerMinNameUpdated = "intake-min-runner-upd" + intakeRunnerMaxName = "intake-max-runner" + intakeRunnerMaxNameUpdated = "intake-max-runner-upd" +) + +var testConfigVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(intakeRunnerMinName), +} + +var testConfigVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(intakeRunnerMaxName), +} + +func testConfigVarsMinUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsMin)) + maps.Copy(tempConfig, testConfigVarsMin) + tempConfig["name"] = config.StringVariable(intakeRunnerMinNameUpdated) + return tempConfig +} + +func testConfigVarsMaxUpdated() config.Variables { + tempConfig := make(config.Variables, len(testConfigVarsMax)) + maps.Copy(tempConfig, testConfigVarsMax) + tempConfig["name"] = config.StringVariable(intakeRunnerMaxNameUpdated) + return tempConfig +} + +func TestAccIntakeRunnerMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIntakeRunnerDestroy, + Steps: []resource.TestStep{ + // Create the minimum runner from the HCL file + { + ConfigVariables: testConfigVarsMin, + Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + Check: resource.ComposeAggregateTestCheckFunc( + // Verify project_id, name and the existence of runner_id + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinName), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), + ), + }, + // Data source check: creates config that includes resource and data source + { + ConfigVariables: testConfigVarsMin, + Config: fmt.Sprintf(` + %s + data "stackit_intake_runner" "example" { + project_id = %s.project_id + runner_id = %s.runner_id + region = %s.region + }`, testutil.IntakeProviderConfig()+"\n"+resourceMin, intakeRunnerResource, intakeRunnerResource, intakeRunnerResource), + Check: resource.ComposeAggregateTestCheckFunc( + // Make sure it's correctly found resource by comparing runner_id attribute + resource.TestCheckResourceAttrPair(intakeRunnerResource, "runner_id", "data.stackit_intake_runner.example", "runner_id"), + ), + }, + // Simulate terraform import + { + ConfigVariables: testConfigVarsMin, + Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + ResourceName: intakeRunnerResource, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + // Construct ID string + r, ok := s.RootModule().Resources[intakeRunnerResource] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", intakeRunnerResource) + } + return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["runner_id"]), nil + }, + }, + // Update check: verifies API updated resource name without crashing + { + ConfigVariables: testConfigVarsMinUpdated(), + Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinNameUpdated), + ), + }, + }, + }) +} + +func TestAccIntakeRunnerMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckIntakeRunnerDestroy, + Steps: []resource.TestStep{ + // Create the max intake runner from HCL files and verify comparison + { + ConfigVariables: testConfigVarsMax, + Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMaxName), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), + ), + }, + // Update and verify changes are reflected + { + ConfigVariables: testConfigVarsMaxUpdated(), + Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMaxNameUpdated), + ), + }, + }, + }) +} + +// testAccCheckIntakeRunnerDestroy act as independent auditor to verify destroy operation +func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { + // Create own raw API client + ctx := context.Background() + var client *intake.APIClient + var err error + + // todo: check this again + effectiveRegion := testutil.Region + if effectiveRegion == "" { + effectiveRegion = "eu01" + } + + if testutil.IntakeCustomEndpoint == "" { + client, err = intake.NewAPIClient(sdkConfig.WithRegion(effectiveRegion)) + } else { + client, err = intake.NewAPIClient( + sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint), + sdkConfig.WithRegion(effectiveRegion), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + // Loop through resources that should have been deleted + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_intake_runner" { + continue + } + + pID := rs.Primary.Attributes["project_id"] + reg := rs.Primary.Attributes["region"] + rID := rs.Primary.Attributes["runner_id"] + + // If it still exists, destroy operation was unsuccessful + _, err := client.GetIntakeRunner(ctx, pID, reg, rID).Execute() + if err == nil { + // Delete to prevent orphaned instances + errDel := client.DeleteIntakeRunner(ctx, pID, reg, rID).Execute() + if errDel != nil { + return fmt.Errorf("resource leaked and manual cleanup failed: %w", errDel) + } + + return fmt.Errorf("intake runner %s still exists in region %s", rID, reg) + } + + var oapiErr *oapierror.GenericOpenAPIError + if !errors.As(err, &oapiErr) || oapiErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("unexpected error checking destruction: %w", err) + } + } + return nil +} diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index 40b02599b..ebea2b85a 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -501,14 +501,12 @@ func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, er payload.Description = conversion.StringValueToPointer(model.Description) var labels map[string]string - if !model.Labels.IsUnknown() { - if !model.Labels.IsNull() { - diags := model.Labels.ElementsAs(context.Background(), &labels, false) - if diags.HasError() { - return nil, fmt.Errorf("failed to convert labels: %w", core.DiagsToError(diags)) - } - payload.Labels = &labels + if !model.Labels.IsUnknown() && !model.Labels.IsNull() { + diags := model.Labels.ElementsAs(context.Background(), &labels, false) + if diags.HasError() { + return nil, fmt.Errorf("failed to convert labels: %w", core.DiagsToError(diags)) } + payload.Labels = &labels } return payload, nil diff --git a/stackit/internal/services/intake/runner/resource_acc_test.go b/stackit/internal/services/intake/runner/resource_acc_test.go deleted file mode 100644 index 948c879ad..000000000 --- a/stackit/internal/services/intake/runner/resource_acc_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package runner_test - -import ( - "context" - "errors" - "fmt" - "net/http" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/intake" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// intakeRunnerResource is the name of the test resource -const intakeRunnerResource = "stackit_intake_runner.example" - -func TestAccIntakeRunner(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckIntakeRunnerDestroy, - Steps: []resource.TestStep{ - // create the runner - { - Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigMinimal("example-runner-minimal"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-minimal"), - resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), - resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), - ), - }, - // update the runner - { - Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigFull("example-runner-full", "An example runner for Intake", 1024, 1100), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-full"), - resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), - ), - }, - // importing the runner - { - ResourceName: intakeRunnerResource, - ImportState: true, - ImportStateVerify: true, - }, - // update to remove optional attributes - { - Config: testutil.IntakeProviderConfig() + testAccIntakeRunnerConfigUpdated("example-runner-updated", 1024, 1100), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "name", "example-runner-updated"), - resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), - ), - }, - }, - }) -} - -func testAccIntakeRunnerConfigMinimal(name string) string { - return fmt.Sprintf(` - resource "stackit_intake_runner" "example" { - project_id = "%s" - name = "%s" - max_message_size_kib = 1024 - max_messages_per_hour = 1000 - } - `, - testutil.ProjectId, - name, - ) -} - -func testAccIntakeRunnerConfigFull(name, description string, maxKib, maxPerHour int) string { - return fmt.Sprintf(` - resource "stackit_intake_runner" "example" { - project_id = "%s" - name = "%s" - description = "%s" - max_message_size_kib = %d - max_messages_per_hour = %d - labels = { - "created_by" = "terraform-provider-stackit" - "env" = "development" - } - } - `, - testutil.ProjectId, - name, - description, - maxKib, - maxPerHour, - ) -} - -func testAccIntakeRunnerConfigUpdated(name string, maxKib, maxPerHour int) string { - return fmt.Sprintf(` - resource "stackit_intake_runner" "example" { - project_id = "%s" - name = "%s" - description = "" - max_message_size_kib = %d - max_messages_per_hour = %d - labels = {} - } - `, - testutil.ProjectId, - name, - maxKib, - maxPerHour, - ) -} - -func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { - ctx := context.Background() - var client *intake.APIClient - var err error - if testutil.IntakeCustomEndpoint == "" { - client, err = intake.NewAPIClient() - } else { - client, err = intake.NewAPIClient(sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint)) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_intake_runner" { - continue - } - // Try to find the runner - _, err := client.GetIntakeRunner(ctx, rs.Primary.Attributes["project_id"], rs.Primary.Attributes["region"], rs.Primary.Attributes["runner_id"]).Execute() - if err == nil { - err = client.DeleteIntakeRunner(ctx, rs.Primary.Attributes["project_id"], rs.Primary.Attributes["region"], rs.Primary.Attributes["runner_id"]).Execute() - if err != nil { - return fmt.Errorf("intake runner with ID %s still existed, got an error removing", rs.Primary.ID, err) - } - - return fmt.Errorf("intake runner with ID %s still existed", rs.Primary.ID) - } - var oapiErr *oapierror.GenericOpenAPIError - if !errors.As(err, &oapiErr) || oapiErr.StatusCode != http.StatusNotFound { - return fmt.Errorf("expected 404 not found, got error: %w", err) - } - } - - return nil -} diff --git a/stackit/internal/services/intake/testdata/resource-max.tf b/stackit/internal/services/intake/testdata/resource-max.tf new file mode 100644 index 000000000..5030bb196 --- /dev/null +++ b/stackit/internal/services/intake/testdata/resource-max.tf @@ -0,0 +1,15 @@ + +variable "project_id" {} +variable "name" {} + +resource "stackit_intake_runner" "example" { + project_id = var.project_id + name = var.name + description = "An example runner for Intake" + max_message_size_kib = 1024 + max_messages_per_hour = 1100 + labels = { + "created_by" = "terraform-provider-stackit" + "env" = "development" + } +} diff --git a/stackit/internal/services/intake/testdata/resource-min.tf b/stackit/internal/services/intake/testdata/resource-min.tf new file mode 100644 index 000000000..3760c61e8 --- /dev/null +++ b/stackit/internal/services/intake/testdata/resource-min.tf @@ -0,0 +1,10 @@ + +variable "project_id" {} +variable "name" {} + +resource "stackit_intake_runner" "example" { + project_id = var.project_id + name = var.name + max_message_size_kib = 1024 + max_messages_per_hour = 1000 +} From d0c4296d45a75934721d91bfaebd0b0bb796c75b Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 16:52:48 +0100 Subject: [PATCH 13/21] Remove replace directive during name change & complement test checks --- stackit/internal/services/intake/resource_acc_test.go | 10 ++++++++++ stackit/internal/services/intake/runner/resource.go | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go index 4c8cb9fd9..a171c7bf3 100644 --- a/stackit/internal/services/intake/resource_acc_test.go +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -71,6 +71,11 @@ func TestAccIntakeRunnerMin(t *testing.T) { resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinName), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1000"), + // Verify empty fields + resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), ), }, // Data source check: creates config that includes resource and data source @@ -129,6 +134,11 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMaxName), resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), + // Verify map size + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), ), }, // Update and verify changes are reflected diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index ebea2b85a..fd17024bd 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -156,9 +156,6 @@ func (r *runnerResource) Schema(_ context.Context, _ resource.SchemaRequest, res "name": schema.StringAttribute{ Description: descriptions["name"], Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "description": schema.StringAttribute{ Description: descriptions["description"], From 4a5a3f6ccb90c1e8c9c2cd84f3aa8cfb904f1293 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:08:46 +0100 Subject: [PATCH 14/21] docs --- docs/data-sources/intake_runner.md | 4 ++ docs/ephemeral-resources/access_token.md | 73 ------------------------ docs/resources/volume.md | 2 +- 3 files changed, 5 insertions(+), 74 deletions(-) delete mode 100644 docs/ephemeral-resources/access_token.md diff --git a/docs/data-sources/intake_runner.md b/docs/data-sources/intake_runner.md index 60950b2f1..bb995f4a7 100644 --- a/docs/data-sources/intake_runner.md +++ b/docs/data-sources/intake_runner.md @@ -20,6 +20,10 @@ Datasource for STACKIT Intake Runner. - `project_id` (String) STACKIT Project ID to which the runner is associated. - `runner_id` (String) The runner ID. +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + ### Read-Only - `description` (String) The description of the runner. diff --git a/docs/ephemeral-resources/access_token.md b/docs/ephemeral-resources/access_token.md deleted file mode 100644 index b45fd715e..000000000 --- a/docs/ephemeral-resources/access_token.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "stackit_access_token Ephemeral Resource - stackit" -subcategory: "" -description: |- - Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Access tokens generated from service account keys expire after 60 minutes. - ~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error. - ~> This ephemeral-resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. ---- - -# stackit_access_token (Ephemeral Resource) - -Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. Access tokens generated from service account keys expire after 60 minutes. - -~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). If any other authentication method is configured, this ephemeral resource will fail with an error. - -~> This ephemeral-resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. - -## Example Usage - -```terraform -provider "stackit" { - default_region = "eu01" - service_account_key_path = "/path/to/sa_key.json" - enable_beta_resources = true -} - -ephemeral "stackit_access_token" "example" {} - -locals { - stackit_api_base_url = "https://iaas.api.stackit.cloud" - public_ip_path = "/v2/projects/${var.project_id}/regions/${var.region}/public-ips" - - public_ip_payload = { - labels = { - key = "value" - } - } -} - -# Docs: https://registry.terraform.io/providers/Mastercard/restapi/latest -provider "restapi" { - uri = local.stackit_api_base_url - write_returns_object = true - - headers = { - Authorization = "Bearer ${ephemeral.stackit_access_token.example.access_token}" - Content-Type = "application/json" - } - - create_method = "POST" - update_method = "PATCH" - destroy_method = "DELETE" -} - -resource "restapi_object" "public_ip_restapi" { - path = local.public_ip_path - data = jsonencode(local.public_ip_payload) - - id_attribute = "id" - read_method = "GET" - create_method = "POST" - update_method = "PATCH" - destroy_method = "DELETE" -} -``` - - -## Schema - -### Read-Only - -- `access_token` (String, Sensitive) JWT access token for STACKIT API authentication. diff --git a/docs/resources/volume.md b/docs/resources/volume.md index 125fed296..fb57dff6d 100644 --- a/docs/resources/volume.md +++ b/docs/resources/volume.md @@ -72,7 +72,7 @@ Required: Optional: - `key_payload_base64` (String, Sensitive) Optional predefined secret, which will be encrypted against the key-encryption-key within the STACKIT-KMS. If not defined, a random secret will be generated by the API and encrypted against the STACKIT-KMS. If a key-payload is provided here, it must be base64 encoded. -- `key_payload_base64_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Optional predefined secret, which will be encrypted against the key-encryption-key within the STACKIT-KMS. If not defined, a random secret will be generated by the API and encrypted against the STACKIT-KMS. If a key-payload is provided here, it must be base64 encoded. +- `key_payload_base64_wo` (String, Sensitive) Optional predefined secret, which will be encrypted against the key-encryption-key within the STACKIT-KMS. If not defined, a random secret will be generated by the API and encrypted against the STACKIT-KMS. If a key-payload is provided here, it must be base64 encoded. - `key_payload_base64_wo_version` (Number) Used together with `key_payload_base64_wo` to trigger an re-create. Increment this value when an update to `key_payload_base64_wo` is required. From 700b2265d339eca36eed5e401f94c23fcf402ab2 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:11:34 +0100 Subject: [PATCH 15/21] lint --- .../services/intake/testdata/resource-max.tf | 18 +++++++++--------- .../services/intake/testdata/resource-min.tf | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/stackit/internal/services/intake/testdata/resource-max.tf b/stackit/internal/services/intake/testdata/resource-max.tf index 5030bb196..ff8324311 100644 --- a/stackit/internal/services/intake/testdata/resource-max.tf +++ b/stackit/internal/services/intake/testdata/resource-max.tf @@ -3,13 +3,13 @@ variable "project_id" {} variable "name" {} resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = var.name - description = "An example runner for Intake" - max_message_size_kib = 1024 - max_messages_per_hour = 1100 - labels = { - "created_by" = "terraform-provider-stackit" - "env" = "development" - } + project_id = var.project_id + name = var.name + description = "An example runner for Intake" + max_message_size_kib = 1024 + max_messages_per_hour = 1100 + labels = { + "created_by" = "terraform-provider-stackit" + "env" = "development" + } } diff --git a/stackit/internal/services/intake/testdata/resource-min.tf b/stackit/internal/services/intake/testdata/resource-min.tf index 3760c61e8..29673b437 100644 --- a/stackit/internal/services/intake/testdata/resource-min.tf +++ b/stackit/internal/services/intake/testdata/resource-min.tf @@ -3,8 +3,8 @@ variable "project_id" {} variable "name" {} resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = var.name - max_message_size_kib = 1024 - max_messages_per_hour = 1000 + project_id = var.project_id + name = var.name + max_message_size_kib = 1024 + max_messages_per_hour = 1000 } From d6a5753f458aa863573f154e2d5670e27884e6dd Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:12:18 +0100 Subject: [PATCH 16/21] Adjust resource_test --- .../services/intake/runner/resource_test.go | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/stackit/internal/services/intake/runner/resource_test.go b/stackit/internal/services/intake/runner/resource_test.go index 921349c64..9d721d462 100644 --- a/stackit/internal/services/intake/runner/resource_test.go +++ b/stackit/internal/services/intake/runner/resource_test.go @@ -1,7 +1,6 @@ package runner import ( - "context" "fmt" "testing" @@ -20,6 +19,7 @@ func TestMapFields(t *testing.T) { description string input *intake.IntakeRunnerResponse model *Model + region string expected *Model wantErr bool }{ @@ -35,8 +35,8 @@ func TestMapFields(t *testing.T) { }, &Model{ ProjectId: types.StringValue("pid"), - Region: types.StringValue("eu01"), }, + "eu01", &Model{ Id: types.StringValue(fmt.Sprintf("pid,eu01,%s", runnerId)), ProjectId: types.StringValue("pid"), @@ -54,6 +54,7 @@ func TestMapFields(t *testing.T) { "nil input", nil, &Model{}, + "eu01", nil, true, }, @@ -61,6 +62,7 @@ func TestMapFields(t *testing.T) { "nil model", &intake.IntakeRunnerResponse{}, nil, + "eu01", nil, true, }, @@ -72,8 +74,8 @@ func TestMapFields(t *testing.T) { }, &Model{ ProjectId: types.StringValue("pid"), - Region: types.StringValue("eu01"), }, + "eu01", &Model{ Id: types.StringValue("pid,eu01,"), ProjectId: types.StringValue("pid"), @@ -90,7 +92,7 @@ func TestMapFields(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.input, tt.model) + err := mapFields(tt.input, tt.model, tt.region) if (err != nil) != tt.wantErr { t.Errorf("mapFields error = %v, wantErr %v", err, tt.wantErr) return @@ -209,10 +211,7 @@ func TestToUpdatePayload(t *testing.T) { "empty model", &Model{}, &Model{}, - &intake.UpdateIntakeRunnerPayload{ - Description: utils.Ptr(""), - Labels: &map[string]string{}, - }, + &intake.UpdateIntakeRunnerPayload{}, false, }, { @@ -231,14 +230,6 @@ func TestToUpdatePayload(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - var labels map[string]string - if tt.model != nil && !tt.model.Labels.IsNull() && !tt.model.Labels.IsUnknown() { - diags := tt.model.Labels.ElementsAs(context.Background(), &labels, false) - if diags.HasError() { - t.Fatalf("error preparing test %v", diags) - } - } - payload, err := toUpdatePayload(tt.model, tt.state) if (err != nil) != tt.wantErr { t.Errorf("toUpdatePayload error = %v, wantErr %v", err, tt.wantErr) From 5d72b4bc6ad76add4af100f53c066f5b180c47fc Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:20:56 +0100 Subject: [PATCH 17/21] adjust last small tweaks --- stackit/internal/services/intake/runner/resource.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index fd17024bd..2222a5790 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -435,17 +435,13 @@ func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model, region str model.Labels = labels } - if runnerResp.Id != nil && *runnerResp.Id == "" { + if runnerResp.Id != nil || *runnerResp.Id == "" { model.RunnerId = types.StringNull() } else { model.RunnerId = types.StringPointerValue(runnerResp.Id) } model.Name = types.StringPointerValue(runnerResp.DisplayName) - if runnerResp.Description == nil { - model.Description = types.StringValue("") - } else { - model.Description = types.StringPointerValue(runnerResp.Description) - } + model.Description = types.StringPointerValue(runnerResp.Description) model.Region = types.StringValue(region) model.MaxMessageSizeKiB = types.Int64PointerValue(runnerResp.MaxMessageSizeKiB) model.MaxMessagesPerHour = types.Int64PointerValue(runnerResp.MaxMessagesPerHour) @@ -498,7 +494,7 @@ func toUpdatePayload(model, state *Model) (*intake.UpdateIntakeRunnerPayload, er payload.Description = conversion.StringValueToPointer(model.Description) var labels map[string]string - if !model.Labels.IsUnknown() && !model.Labels.IsNull() { + if !model.Labels.IsNull() && !model.Labels.IsUnknown() { diags := model.Labels.ElementsAs(context.Background(), &labels, false) if diags.HasError() { return nil, fmt.Errorf("failed to convert labels: %w", core.DiagsToError(diags)) From 1f0851ddb224ab2b99cfbbc3a02f3856113df610 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:25:13 +0100 Subject: [PATCH 18/21] lint --- docs/resources/intake_runner.md | 12 ++++++------ .../resources/stackit_intake_runner/resource.tf | 12 ++++++------ .../services/intake/testdata/resource-max.tf | 16 ++++++++-------- .../services/intake/testdata/resource-min.tf | 10 +++++----- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/resources/intake_runner.md b/docs/resources/intake_runner.md index 9a246d535..215b5316f 100644 --- a/docs/resources/intake_runner.md +++ b/docs/resources/intake_runner.md @@ -14,16 +14,16 @@ Manages STACKIT Intake Runner. ```terraform resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = "example-runner-full" - description = "An example runner for STACKIT Intake" - max_message_size_kib = 2048 - max_messages_per_hour = 1500 + project_id = var.project_id + name = "example-runner-full" + description = "An example runner for STACKIT Intake" + max_message_size_kib = 2048 + max_messages_per_hour = 1500 labels = { "created_by" = "terraform-example" "env" = "production" } - region = var.region + region = var.region } import { diff --git a/examples/resources/stackit_intake_runner/resource.tf b/examples/resources/stackit_intake_runner/resource.tf index ceda583ff..311a9f41f 100644 --- a/examples/resources/stackit_intake_runner/resource.tf +++ b/examples/resources/stackit_intake_runner/resource.tf @@ -1,14 +1,14 @@ resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = "example-runner-full" - description = "An example runner for STACKIT Intake" - max_message_size_kib = 2048 - max_messages_per_hour = 1500 + project_id = var.project_id + name = "example-runner-full" + description = "An example runner for STACKIT Intake" + max_message_size_kib = 2048 + max_messages_per_hour = 1500 labels = { "created_by" = "terraform-example" "env" = "production" } - region = var.region + region = var.region } import { diff --git a/stackit/internal/services/intake/testdata/resource-max.tf b/stackit/internal/services/intake/testdata/resource-max.tf index ff8324311..5614426bb 100644 --- a/stackit/internal/services/intake/testdata/resource-max.tf +++ b/stackit/internal/services/intake/testdata/resource-max.tf @@ -3,13 +3,13 @@ variable "project_id" {} variable "name" {} resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = var.name - description = "An example runner for Intake" - max_message_size_kib = 1024 - max_messages_per_hour = 1100 + project_id = var.project_id + name = var.name + description = "An example runner for Intake" + max_message_size_kib = 1024 + max_messages_per_hour = 1100 labels = { - "created_by" = "terraform-provider-stackit" - "env" = "development" + "created_by" = "terraform-provider-stackit" + "env" = "development" } -} +} \ No newline at end of file diff --git a/stackit/internal/services/intake/testdata/resource-min.tf b/stackit/internal/services/intake/testdata/resource-min.tf index 29673b437..e7c8d77fa 100644 --- a/stackit/internal/services/intake/testdata/resource-min.tf +++ b/stackit/internal/services/intake/testdata/resource-min.tf @@ -3,8 +3,8 @@ variable "project_id" {} variable "name" {} resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = var.name - max_message_size_kib = 1024 - max_messages_per_hour = 1000 -} + project_id = var.project_id + name = var.name + max_message_size_kib = 1024 + max_messages_per_hour = 1000 +} \ No newline at end of file From b0e23f693de9dcdf7f75b222d223455d8475084e Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 18:32:45 +0100 Subject: [PATCH 19/21] Add missing checks in tests --- .../services/intake/resource_acc_test.go | 43 +++++++++++++++---- .../services/intake/runner/resource.go | 11 ++--- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go index a171c7bf3..92d12ebb3 100644 --- a/stackit/internal/services/intake/resource_acc_test.go +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -67,15 +67,13 @@ func TestAccIntakeRunnerMin(t *testing.T) { ConfigVariables: testConfigVarsMin, Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, Check: resource.ComposeAggregateTestCheckFunc( - // Verify project_id, name and the existence of runner_id resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinName), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1000"), - // Verify empty fields - resource.TestCheckResourceAttr(intakeRunnerResource, "description", ""), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "0"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "region"), ), }, // Data source check: creates config that includes resource and data source @@ -90,7 +88,11 @@ func TestAccIntakeRunnerMin(t *testing.T) { }`, testutil.IntakeProviderConfig()+"\n"+resourceMin, intakeRunnerResource, intakeRunnerResource, intakeRunnerResource), Check: resource.ComposeAggregateTestCheckFunc( // Make sure it's correctly found resource by comparing runner_id attribute + resource.TestCheckResourceAttrPair(intakeRunnerResource, "project_id", "data.stackit_intake_runner.example", "project_id"), resource.TestCheckResourceAttrPair(intakeRunnerResource, "runner_id", "data.stackit_intake_runner.example", "runner_id"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "name", "data.stackit_intake_runner.example", "name"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "region", "data.stackit_intake_runner.example", "region"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "max_messages_per_hour", "data.stackit_intake_runner.example", "max_messages_per_hour"), ), }, // Simulate terraform import @@ -131,14 +133,35 @@ func TestAccIntakeRunnerMax(t *testing.T) { ConfigVariables: testConfigVarsMax, Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMaxName), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1100"), - // Verify map size resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), - resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "region"), + ), + }, + { + ConfigVariables: testConfigVarsMax, + Config: fmt.Sprintf(` + %s + data "stackit_intake_runner" "example" { + project_id = %s.project_id + runner_id = %s.runner_id + }`, testutil.IntakeProviderConfig()+"\n"+resourceMax, intakeRunnerResource, intakeRunnerResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(intakeRunnerResource, "project_id", "data.stackit_intake_runner.example", "project_id"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "runner_id", "data.stackit_intake_runner.example", "runner_id"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "name", "data.stackit_intake_runner.example", "name"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "description", "data.stackit_intake_runner.example", "description"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "region", "data.stackit_intake_runner.example", "region"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "labels.env", "data.stackit_intake_runner.example", "labels.env"), + resource.TestCheckResourceAttrPair(intakeRunnerResource, "max_messages_per_hour", "data.stackit_intake_runner.example", "max_messages_per_hour"), ), }, // Update and verify changes are reflected @@ -146,7 +169,10 @@ func TestAccIntakeRunnerMax(t *testing.T) { ConfigVariables: testConfigVarsMaxUpdated(), Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMaxNameUpdated), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["name"])), + // Ensure optional fields survived the update (didn't get wiped by a bad Update payload) + resource.TestCheckResourceAttr(intakeRunnerResource, "description", "An example runner for Intake"), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.env", "development"), ), }, }, @@ -160,7 +186,6 @@ func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { var client *intake.APIClient var err error - // todo: check this again effectiveRegion := testutil.Region if effectiveRegion == "" { effectiveRegion = "eu01" diff --git a/stackit/internal/services/intake/runner/resource.go b/stackit/internal/services/intake/runner/resource.go index 2222a5790..c73818bcf 100644 --- a/stackit/internal/services/intake/runner/resource.go +++ b/stackit/internal/services/intake/runner/resource.go @@ -425,6 +425,12 @@ func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model, region str runnerId, ) + if runnerResp.Id == nil || *runnerResp.Id == "" { + model.RunnerId = types.StringNull() + } else { + model.RunnerId = types.StringPointerValue(runnerResp.Id) + } + if runnerResp.Labels == nil { model.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{}) } else { @@ -435,11 +441,6 @@ func mapFields(runnerResp *intake.IntakeRunnerResponse, model *Model, region str model.Labels = labels } - if runnerResp.Id != nil || *runnerResp.Id == "" { - model.RunnerId = types.StringNull() - } else { - model.RunnerId = types.StringPointerValue(runnerResp.Id) - } model.Name = types.StringPointerValue(runnerResp.DisplayName) model.Description = types.StringPointerValue(runnerResp.Description) model.Region = types.StringValue(region) From f3a32c0aa94eff30edb734a72ef938897b88e33d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 18:46:04 +0100 Subject: [PATCH 20/21] Fix remaining test failing in resource_test.go --- stackit/internal/services/intake/runner/resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/intake/runner/resource_test.go b/stackit/internal/services/intake/runner/resource_test.go index 9d721d462..b6d3594a2 100644 --- a/stackit/internal/services/intake/runner/resource_test.go +++ b/stackit/internal/services/intake/runner/resource_test.go @@ -82,7 +82,7 @@ func TestMapFields(t *testing.T) { Region: types.StringValue("eu01"), RunnerId: types.StringNull(), Name: types.StringNull(), - Description: types.StringValue(""), + Description: types.StringNull(), Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), MaxMessageSizeKiB: types.Int64Null(), MaxMessagesPerHour: types.Int64Null(), From a2fdb55217e2aa3ab083aa98773bdc6b7dad010f Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Tue, 27 Jan 2026 15:52:57 +0100 Subject: [PATCH 21/21] Adjust destroy function --- .../services/intake/resource_acc_test.go | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go index 92d12ebb3..5adb68761 100644 --- a/stackit/internal/services/intake/resource_acc_test.go +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -3,18 +3,19 @@ package intake_test import ( "context" _ "embed" - "errors" "fmt" "maps" - "net/http" + "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/intake" + "github.com/stackitcloud/stackit-sdk-go/services/intake/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -181,53 +182,57 @@ func TestAccIntakeRunnerMax(t *testing.T) { // testAccCheckIntakeRunnerDestroy act as independent auditor to verify destroy operation func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { - // Create own raw API client ctx := context.Background() var client *intake.APIClient var err error - effectiveRegion := testutil.Region - if effectiveRegion == "" { - effectiveRegion = "eu01" - } - if testutil.IntakeCustomEndpoint == "" { - client, err = intake.NewAPIClient(sdkConfig.WithRegion(effectiveRegion)) + client, err = intake.NewAPIClient() } else { client, err = intake.NewAPIClient( sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint), - sdkConfig.WithRegion(effectiveRegion), ) } if err != nil { return fmt.Errorf("creating client: %w", err) } - // Loop through resources that should have been deleted + instancesToDestroy := []string{} for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_intake_runner" { continue } + // Intake internal ID: "[project_id],[region],[runner_id]" + runnerId := strings.Split(rs.Primary.ID, core.Separator)[2] + instancesToDestroy = append(instancesToDestroy, runnerId) + } - pID := rs.Primary.Attributes["project_id"] - reg := rs.Primary.Attributes["region"] - rID := rs.Primary.Attributes["runner_id"] - - // If it still exists, destroy operation was unsuccessful - _, err := client.GetIntakeRunner(ctx, pID, reg, rID).Execute() - if err == nil { - // Delete to prevent orphaned instances - errDel := client.DeleteIntakeRunner(ctx, pID, reg, rID).Execute() - if errDel != nil { - return fmt.Errorf("resource leaked and manual cleanup failed: %w", errDel) - } + // List all resources in the project/region to see what's left + instancesResp, err := client.ListIntakeRunners(ctx, testutil.ProjectId, testutil.Region).Execute() + if err != nil { + return fmt.Errorf("getting instancesResp: %w", err) + } - return fmt.Errorf("intake runner %s still exists in region %s", rID, reg) + // If the API returns a list of runners, check if our deleted ones are still there + items := *instancesResp.IntakeRunners + for i := range items { + if items[i].Id == nil { + continue } - var oapiErr *oapierror.GenericOpenAPIError - if !errors.As(err, &oapiErr) || oapiErr.StatusCode != http.StatusNotFound { - return fmt.Errorf("unexpected error checking destruction: %w", err) + // If a runner we thought we deleted is found in the list + if utils.Contains(instancesToDestroy, *items[i].Id) { + // Attempt a final delete and wait, just like Postgres + err := client.DeleteIntakeRunner(ctx, testutil.ProjectId, testutil.Region, *items[i].Id).Execute() + if err != nil { + return fmt.Errorf("deleting runner %s during CheckDestroy: %w", *items[i].Id, err) + } + + // Using the wait handler for destruction verification + _, err = wait.DeleteIntakeRunnerWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, *items[i].Id).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("deleting runner %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) + } } } return nil