From 47b81cc2ad05b0fbe00dda59a2b88941956455c2 Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Mon, 8 Dec 2025 00:05:21 +0100 Subject: [PATCH 01/27] 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 27290dfe4..a7c271856 100644 --- a/docs/index.md +++ b/docs/index.md @@ -173,6 +173,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 456ddce40..1e657632d 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -43,6 +43,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 85abf49ba..b767f0005 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -58,6 +58,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" @@ -162,6 +163,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"` @@ -341,6 +343,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"], @@ -498,6 +504,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 }) @@ -698,6 +705,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 cd7afd723550f6d8ce8d79e0b5335e768e73ce8f Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Mon, 8 Dec 2025 09:07:58 +0100 Subject: [PATCH 02/27] 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 6cd1576da1819968158d9c478f58230b985afb36 Mon Sep 17 00:00:00 2001 From: Devansh Thakur Date: Tue, 16 Dec 2025 22:19:46 +0100 Subject: [PATCH 03/27] 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 b767f0005..9338477dc 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -619,6 +619,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 d86b63ed380da3fd715c704bb9d8714a8a301a0f Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 11:24:22 +0100 Subject: [PATCH 04/27] Resolve conflicts # Conflicts: # go.sum # Conflicts: # go.mod # go.sum # Conflicts: # go.mod # go.sum --- go.mod | 1 + stackit/internal/testutil/testutil.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/go.mod b/go.mod index 048e496ba..014fbc0f6 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/git v0.10.3 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.5 github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2 + github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.2 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.7.3 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.6 github.com/stackitcloud/stackit-sdk-go/services/logs v0.5.2 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 dc989ee2604724b1294711dc65a115c518e966ec Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 11:33:04 +0100 Subject: [PATCH 05/27] 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 68b9ee4ca71a560c167ceb53b96a3805ec1918fb Mon Sep 17 00:00:00 2001 From: BipBopBipBop Date: Thu, 22 Jan 2026 11:56:50 +0100 Subject: [PATCH 06/27] 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 42eeda6224a95d8bf7400104fffa6f48bb2f16b5 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:12:32 +0100 Subject: [PATCH 07/27] 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 a7c271856..93af95f45 100644 --- a/docs/index.md +++ b/docs/index.md @@ -173,7 +173,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 c30eb6a16ceeda1048e898a829309ddb802503b4 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:12:50 +0100 Subject: [PATCH 08/27] Add description to custom endpoint --- stackit/provider.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stackit/provider.go b/stackit/provider.go index 9338477dc..04b084f01 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -217,6 +217,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", @@ -245,6 +246,7 @@ func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *pro "enable_beta_resources": "Enable beta resources. Default is false.", "experiments": fmt.Sprintf("Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: %v", strings.Join(features.AvailableExperiments, ", ")), } + resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "credentials_path": schema.StringAttribute{ From 139027af1dbc5be4cd516445fb5a6db1c7078162 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 22 Jan 2026 17:47:36 +0100 Subject: [PATCH 09/27] 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 07ae9aaad991ef48c4d60f389ae94df1c7603e5f Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 13:33:43 +0100 Subject: [PATCH 10/27] 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 ccab9c7b6dd2a351c1f0a98eb48f6f16b303ea58 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 13:51:35 +0100 Subject: [PATCH 11/27] 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 70d08904f3b941fe83bddeac2df35915e6213111 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 14:45:43 +0100 Subject: [PATCH 12/27] 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 e9e819b7cdfb6d12701200084787d9f4bf1d38c6 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 16:52:48 +0100 Subject: [PATCH 13/27] 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 c175accd15b3eaac28b830f1a60516d0356bc32d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:08:46 +0100 Subject: [PATCH 14/27] 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 3b40d7c8e0f4314c504e6aa09fbf626637e2c98d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:11:34 +0100 Subject: [PATCH 15/27] 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 dbdb4f74c84bc397216e0db088e692ca6dc444f8 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:12:18 +0100 Subject: [PATCH 16/27] 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 af76741c4873976efc29b5e30ce7a22eb797f863 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:20:56 +0100 Subject: [PATCH 17/27] 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 0a779a8f157786c2c2e1f2c3b5a4d58e4f802e15 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 17:25:13 +0100 Subject: [PATCH 18/27] 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 9feee4082b5ee45b4fc7567292ac7a4f950531c7 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 18:32:45 +0100 Subject: [PATCH 19/27] 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 68044324bcc291d5ac0951ae5bba49ae8d4a8ccb Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 26 Jan 2026 18:46:04 +0100 Subject: [PATCH 20/27] 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 ded68a1b0717e9d2e216eca82af0aa8a0162fcf0 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Tue, 27 Jan 2026 15:52:57 +0100 Subject: [PATCH 21/27] 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 From ecb66fd703c06fb3a89194a12447de09d744595d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Thu, 29 Jan 2026 10:42:01 +0100 Subject: [PATCH 22/27] Reestablish tf-plugin-docs to newer version and re-generate docs properly --- docs/ephemeral-resources/access_token.md | 73 ++++++++++++++++++++++++ docs/resources/volume.md | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 docs/ephemeral-resources/access_token.md diff --git a/docs/ephemeral-resources/access_token.md b/docs/ephemeral-resources/access_token.md new file mode 100644 index 000000000..b45fd715e --- /dev/null +++ b/docs/ephemeral-resources/access_token.md @@ -0,0 +1,73 @@ +--- +# 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 fb57dff6d..125fed296 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) 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_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 a7964f6f0dd1c3a19f3d2103d9222b1a97b64810 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Tue, 17 Feb 2026 13:49:07 +0100 Subject: [PATCH 23/27] Address review --- .../services/intake/resource_acc_test.go | 131 +++++++++++------- 1 file changed, 79 insertions(+), 52 deletions(-) diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go index 5adb68761..fd964eea1 100644 --- a/stackit/internal/services/intake/resource_acc_test.go +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -20,41 +20,39 @@ import ( ) //go:embed testdata/resource-min.tf -var resourceMin string +var resourceIntakeRunnerMin string //go:embed testdata/resource-max.tf -var resourceMax string +var resourceIntakeRunnerMax 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 testIntakeRunnerConfigVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable("intake-min-runner"), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1000), } -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(intakeRunnerMaxName), +var testIntakeRunnerConfigVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable("intake-max-runner"), + "description": config.StringVariable("An example runner for Intake"), + "max_message_size_kib": config.IntegerVariable(1024), + "max_messages_per_hour": config.IntegerVariable(1100), } -func testConfigVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMin)) - maps.Copy(tempConfig, testConfigVarsMin) - tempConfig["name"] = config.StringVariable(intakeRunnerMinNameUpdated) +func testIntakeRunnerConfigVarsMinUpdated() config.Variables { + tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMin)) + maps.Copy(tempConfig, testIntakeRunnerConfigVarsMin) + tempConfig["name"] = config.StringVariable("intake-min-runner-upd") return tempConfig } -func testConfigVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMax)) - maps.Copy(tempConfig, testConfigVarsMax) - tempConfig["name"] = config.StringVariable(intakeRunnerMaxNameUpdated) +func testIntakeRunnerConfigVarsMaxUpdated() config.Variables { + tempConfig := make(config.Variables, len(testIntakeRunnerConfigVarsMax)) + maps.Copy(tempConfig, testIntakeRunnerConfigVarsMax) + tempConfig["name"] = config.StringVariable("intake-max-runner-upd") return tempConfig } @@ -65,28 +63,28 @@ func TestAccIntakeRunnerMin(t *testing.T) { Steps: []resource.TestStep{ // Create the minimum runner from the HCL file { - ConfigVariables: testConfigVarsMin, - Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + ConfigVariables: testIntakeRunnerConfigVarsMin, + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMin, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinName), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["name"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", "1024"), - resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", "1000"), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), - resource.TestCheckResourceAttrSet(intakeRunnerResource, "region"), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), ), }, // Data source check: creates config that includes resource and data source { - ConfigVariables: testConfigVarsMin, + ConfigVariables: testIntakeRunnerConfigVarsMin, 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), + }`, testutil.IntakeProviderConfig()+"\n"+resourceIntakeRunnerMin, 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"), @@ -98,8 +96,8 @@ func TestAccIntakeRunnerMin(t *testing.T) { }, // Simulate terraform import { - ConfigVariables: testConfigVarsMin, - Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + ConfigVariables: testIntakeRunnerConfigVarsMin, + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMin, ResourceName: intakeRunnerResource, ImportState: true, ImportStateVerify: true, @@ -114,10 +112,16 @@ func TestAccIntakeRunnerMin(t *testing.T) { }, // Update check: verifies API updated resource name without crashing { - ConfigVariables: testConfigVarsMinUpdated(), - Config: testutil.IntakeProviderConfig() + "\n" + resourceMin, + ConfigVariables: testIntakeRunnerConfigVarsMinUpdated(), + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMin, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "name", intakeRunnerMinNameUpdated), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMinUpdated()["name"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), + resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), ), }, }, @@ -131,30 +135,30 @@ func TestAccIntakeRunnerMax(t *testing.T) { Steps: []resource.TestStep{ // Create the max intake runner from HCL files and verify comparison { - ConfigVariables: testConfigVarsMax, - Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, + ConfigVariables: testIntakeRunnerConfigVarsMax, + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMax, Check: resource.ComposeAggregateTestCheckFunc( - 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"), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["name"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["description"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_messages_per_hour"])), resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), 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"), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), ), }, { - ConfigVariables: testConfigVarsMax, + ConfigVariables: testIntakeRunnerConfigVarsMax, 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), + }`, testutil.IntakeProviderConfig()+"\n"+resourceIntakeRunnerMax, 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"), @@ -165,15 +169,38 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttrPair(intakeRunnerResource, "max_messages_per_hour", "data.stackit_intake_runner.example", "max_messages_per_hour"), ), }, + // Simulate terraform import + { + ConfigVariables: testIntakeRunnerConfigVarsMax, + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMax, + 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 and verify changes are reflected { - ConfigVariables: testConfigVarsMaxUpdated(), - Config: testutil.IntakeProviderConfig() + "\n" + resourceMax, + ConfigVariables: testIntakeRunnerConfigVarsMaxUpdated(), + Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMax, Check: resource.ComposeAggregateTestCheckFunc( - 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, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMaxUpdated()["name"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "description", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["description"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_message_size_kib"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["max_messages_per_hour"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "labels.%", "2"), 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.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), ), }, }, From 4dd0bebdfb946853954b33755b99016786d11f0d Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Tue, 17 Feb 2026 16:16:36 +0100 Subject: [PATCH 24/27] Small adjustment --- .../services/intake/resource_acc_test.go | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/stackit/internal/services/intake/resource_acc_test.go b/stackit/internal/services/intake/resource_acc_test.go index fd964eea1..31c8fb2cc 100644 --- a/stackit/internal/services/intake/resource_acc_test.go +++ b/stackit/internal/services/intake/resource_acc_test.go @@ -8,11 +8,12 @@ import ( "strings" "testing" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "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/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" @@ -30,6 +31,7 @@ const intakeRunnerResource = "stackit_intake_runner.example" var testIntakeRunnerConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable("intake-min-runner"), + "region": config.StringVariable(testutil.Region), "max_message_size_kib": config.IntegerVariable(1024), "max_messages_per_hour": config.IntegerVariable(1000), } @@ -37,6 +39,7 @@ var testIntakeRunnerConfigVarsMin = config.Variables{ var testIntakeRunnerConfigVarsMax = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable("intake-max-runner"), + "region": config.StringVariable(testutil.Region), "description": config.StringVariable("An example runner for Intake"), "max_message_size_kib": config.IntegerVariable(1024), "max_messages_per_hour": config.IntegerVariable(1100), @@ -72,19 +75,19 @@ func TestAccIntakeRunnerMin(t *testing.T) { resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), ), }, // Data source check: creates config that includes resource and data source { ConfigVariables: testIntakeRunnerConfigVarsMin, 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"+resourceIntakeRunnerMin, intakeRunnerResource, intakeRunnerResource, intakeRunnerResource), + %s + data "stackit_intake_runner" "example" { + project_id = %s.project_id + runner_id = %s.runner_id + region = %s.region + }`, testutil.IntakeProviderConfig()+"\n"+resourceIntakeRunnerMin, 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"), @@ -107,6 +110,7 @@ func TestAccIntakeRunnerMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource %s", intakeRunnerResource) } + // ID structure: project_id, region, runner_id return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["runner_id"]), nil }, }, @@ -115,11 +119,11 @@ func TestAccIntakeRunnerMin(t *testing.T) { ConfigVariables: testIntakeRunnerConfigVarsMinUpdated(), Config: testutil.IntakeProviderConfig() + "\n" + resourceIntakeRunnerMin, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr(intakeRunnerResource, "project_id", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMinUpdated()["project_id"])), resource.TestCheckResourceAttr(intakeRunnerResource, "name", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMinUpdated()["name"])), resource.TestCheckResourceAttr(intakeRunnerResource, "max_message_size_kib", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_message_size_kib"])), resource.TestCheckResourceAttr(intakeRunnerResource, "max_messages_per_hour", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMin["max_messages_per_hour"])), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), ), @@ -148,17 +152,17 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), ), }, { ConfigVariables: testIntakeRunnerConfigVarsMax, Config: fmt.Sprintf(` - %s - data "stackit_intake_runner" "example" { - project_id = %s.project_id - runner_id = %s.runner_id - }`, testutil.IntakeProviderConfig()+"\n"+resourceIntakeRunnerMax, intakeRunnerResource, intakeRunnerResource), + %s + data "stackit_intake_runner" "example" { + project_id = %s.project_id + runner_id = %s.runner_id + }`, testutil.IntakeProviderConfig()+"\n"+resourceIntakeRunnerMax, 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"), @@ -182,6 +186,7 @@ func TestAccIntakeRunnerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find resource %s", intakeRunnerResource) } + // ID structure: project_id, region, runner_id return fmt.Sprintf("%s,%s,%s", r.Primary.Attributes["project_id"], r.Primary.Attributes["region"], r.Primary.Attributes["runner_id"]), nil }, }, @@ -200,7 +205,7 @@ func TestAccIntakeRunnerMax(t *testing.T) { resource.TestCheckResourceAttr(intakeRunnerResource, "labels.created_by", "terraform-provider-stackit"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "runner_id"), resource.TestCheckResourceAttrSet(intakeRunnerResource, "id"), - resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.Region), + resource.TestCheckResourceAttr(intakeRunnerResource, "region", testutil.ConvertConfigVariable(testIntakeRunnerConfigVarsMax["region"])), ), }, }, @@ -214,7 +219,9 @@ func testAccCheckIntakeRunnerDestroy(s *terraform.State) error { var err error if testutil.IntakeCustomEndpoint == "" { - client, err = intake.NewAPIClient() + client, err = intake.NewAPIClient( + sdkConfig.WithRegion(testutil.Region), + ) } else { client, err = intake.NewAPIClient( sdkConfig.WithEndpoint(testutil.IntakeCustomEndpoint), From 87f2376f6ae0fb4ace350e52fd3dd453c811f12a Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Fri, 20 Feb 2026 16:48:48 +0100 Subject: [PATCH 25/27] Small adjustment and complete docs --- docs/data-sources/intake_runner.md | 9 ++++++++- docs/resources/intake_runner.md | 18 ++++++------------ .../stackit_intake_runner/data-source.tf | 4 ++++ .../stackit_intake_runner/resource.tf | 18 ++++++------------ .../services/intake/testdata/resource-max.tf | 2 ++ .../services/intake/testdata/resource-min.tf | 2 ++ 6 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 examples/data-sources/stackit_intake_runner/data-source.tf diff --git a/docs/data-sources/intake_runner.md b/docs/data-sources/intake_runner.md index bb995f4a7..52806a24a 100644 --- a/docs/data-sources/intake_runner.md +++ b/docs/data-sources/intake_runner.md @@ -10,7 +10,14 @@ description: |- Datasource for STACKIT Intake Runner. - +## Example Usage + +```terraform +data "stackit_intake_runner" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + runner_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` ## Schema diff --git a/docs/resources/intake_runner.md b/docs/resources/intake_runner.md index 215b5316f..4dd64b6ab 100644 --- a/docs/resources/intake_runner.md +++ b/docs/resources/intake_runner.md @@ -14,21 +14,15 @@ Manages STACKIT Intake Runner. ```terraform resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = "example-runner-full" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-runner" + region = "eu01" description = "An example runner for STACKIT Intake" - max_message_size_kib = 2048 - max_messages_per_hour = 1500 + max_message_size_kib = 1024 + max_messages_per_hour = 1000 labels = { - "created_by" = "terraform-example" - "env" = "production" + "env" = "development" } - region = var.region -} - -import { - to = stackit_intake_runner.example - id = "${var.project_id},${var.region},${var.runner_id}" } ``` diff --git a/examples/data-sources/stackit_intake_runner/data-source.tf b/examples/data-sources/stackit_intake_runner/data-source.tf new file mode 100644 index 000000000..0c6ea2288 --- /dev/null +++ b/examples/data-sources/stackit_intake_runner/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_intake_runner" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + runner_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_intake_runner/resource.tf b/examples/resources/stackit_intake_runner/resource.tf index 311a9f41f..d85980bce 100644 --- a/examples/resources/stackit_intake_runner/resource.tf +++ b/examples/resources/stackit_intake_runner/resource.tf @@ -1,17 +1,11 @@ resource "stackit_intake_runner" "example" { - project_id = var.project_id - name = "example-runner-full" + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-runner" + region = "eu01" description = "An example runner for STACKIT Intake" - max_message_size_kib = 2048 - max_messages_per_hour = 1500 + max_message_size_kib = 1024 + max_messages_per_hour = 1000 labels = { - "created_by" = "terraform-example" - "env" = "production" + "env" = "development" } - region = var.region } - -import { - to = stackit_intake_runner.example - id = "${var.project_id},${var.region},${var.runner_id}" -} \ No newline at end of file diff --git a/stackit/internal/services/intake/testdata/resource-max.tf b/stackit/internal/services/intake/testdata/resource-max.tf index 5614426bb..9e5ff9e9f 100644 --- a/stackit/internal/services/intake/testdata/resource-max.tf +++ b/stackit/internal/services/intake/testdata/resource-max.tf @@ -1,10 +1,12 @@ variable "project_id" {} variable "name" {} +variable "region" {} resource "stackit_intake_runner" "example" { project_id = var.project_id name = var.name + region = var.region description = "An example runner for Intake" max_message_size_kib = 1024 max_messages_per_hour = 1100 diff --git a/stackit/internal/services/intake/testdata/resource-min.tf b/stackit/internal/services/intake/testdata/resource-min.tf index e7c8d77fa..7c6f26fc0 100644 --- a/stackit/internal/services/intake/testdata/resource-min.tf +++ b/stackit/internal/services/intake/testdata/resource-min.tf @@ -1,10 +1,12 @@ variable "project_id" {} variable "name" {} +variable "region" {} resource "stackit_intake_runner" "example" { project_id = var.project_id name = var.name + region = var.region max_message_size_kib = 1024 max_messages_per_hour = 1000 } \ No newline at end of file From 7ff4f549b4211ec92b7026223bc715602330d771 Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 23 Feb 2026 09:45:11 +0100 Subject: [PATCH 26/27] Rebase --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 014fbc0f6..066bf21c1 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/edge v0.4.3 github.com/stackitcloud/stackit-sdk-go/services/git v0.10.3 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.5 - github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2 github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.2 + github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.7.3 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.6 github.com/stackitcloud/stackit-sdk-go/services/logs v0.5.2 diff --git a/go.sum b/go.sum index f2ff5e6e4..b3633d1ca 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.10.3 h1:VIjkSofZz9utOOkBd github.com/stackitcloud/stackit-sdk-go/services/git v0.10.3/go.mod h1:EJk1Ss9GTel2NPIu/w3+x9XcQcEd2k3ibea5aQDzVhQ= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.5 h1:W57+XRa8wTLsi5CV9Tqa7mGgt/PvlRM//RurXSmvII8= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.5/go.mod h1:lTWjW57eAq1bwfM6nsNinhoBr3MHFW/GaFasdAsYfDM= +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.3.2 h1:2ulSL2IkIAKND59eAjbEhVkOoBMyvm48ojwz1a3t0U0= github.com/stackitcloud/stackit-sdk-go/services/kms v1.3.2/go.mod h1:cuIaMMiHeHQsbvy7BOFMutoV3QtN+ZBx7Tg3GmYUw7s= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.7.3 h1:d/qIj+XNaqByVbLvwpWoA0Ekv0yrONWyNswg4/jGX7Y= From f2507719b5967e750021c60920954f91187639ce Mon Sep 17 00:00:00 2001 From: Yago Carlos Fernandez Gou Date: Mon, 23 Feb 2026 10:17:32 +0100 Subject: [PATCH 27/27] lint --- stackit/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/provider.go b/stackit/provider.go index 04b084f01..070f8b74d 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -217,7 +217,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", + "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",