Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions stackit/internal/services/rabbitmq/credential/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
Expand Down Expand Up @@ -204,7 +203,14 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
return
}
credentialId := *credentialsResp.Id
ctx = tflog.SetField(ctx, "credential_id", credentialId)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"instance_id": instanceId,
"credential_id": credentialId,
})
if resp.Diagnostics.HasError() {
return
}

waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx)
if err != nil {
Expand Down Expand Up @@ -325,9 +331,14 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"instance_id": idParts[1],
"credential_id": idParts[2],
})
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "RabbitMQ credential state imported")
}

Expand Down
24 changes: 20 additions & 4 deletions stackit/internal/services/rabbitmq/instance/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
Expand Down Expand Up @@ -363,8 +362,20 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques

ctx = core.LogResponse(ctx)

if createResp.InstanceId == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response did not include instance ID")
return
}

instanceId := *createResp.InstanceId
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"instance_id": instanceId,
})
if resp.Diagnostics.HasError() {
return
}

waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err))
Expand Down Expand Up @@ -554,8 +565,13 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"instance_id": idParts[1],
})
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "RabbitMQ instance state imported")
}

Expand Down
194 changes: 194 additions & 0 deletions stackit/internal/services/rabbitmq/rabbitmq_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package rabbitmq_test
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"testing"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/stackitcloud/stackit-sdk-go/core/config"
Expand Down Expand Up @@ -243,6 +245,198 @@ func TestAccRabbitMQResource(t *testing.T) {
})
}

// Run apply for an instance and produce an error in the waiter. By erroring out state checks are not run in this step.
// The second step imports the resource and runs a state check to verify that the first step wrote the IDs, despite the error.
// When importing we append "-import" to the credential ID to verify that the import isn't overwriting the instance
// ID from the first step
func TestRabbitMQInstanceSavesIDsOnError(t *testing.T) {
projectId := uuid.NewString()
instanceId := uuid.NewString()
const (
name = "instance-name"
planName = "plan-name"
planId = "plan-id"
version = "version"
)
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
rabbitmq_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_rabbitmq_instance" "instance" {
project_id = "%s"
name = "%s"
plan_name = "%s"
version = "%s"
}
`, s.Server.URL, projectId, name, planName, version)
offerings := testutil.MockResponse{
ToJsonBody: &rabbitmq.ListOfferingsResponse{
Offerings: &[]rabbitmq.Offering{
{
Version: utils.Ptr(version),
Plans: &[]rabbitmq.Plan{
{
Name: utils.Ptr(planName),
Id: utils.Ptr(planId),
},
},
},
},
},
}
resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(
// respond to listing offerings
offerings,
// initial post response
testutil.MockResponse{
ToJsonBody: rabbitmq.CreateInstanceResponse{
InstanceId: utils.Ptr(instanceId),
},
},
// failing waiter
testutil.MockResponse{
ToJsonBody: rabbitmq.Instance{
Status: utils.Ptr(rabbitmq.INSTANCESTATUS_FAILED),
},
},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating instance.*"),
},
{
PreConfig: func() {
s.Reset(
// read from import
testutil.MockResponse{
ToJsonBody: rabbitmq.Instance{
Status: utils.Ptr(rabbitmq.INSTANCESTATUS_ACTIVE),
InstanceId: utils.Ptr(instanceId + "-import"),
PlanId: utils.Ptr(planId),
},
},
// list offerings in import
offerings,
// delete
testutil.MockResponse{StatusCode: http.StatusAccepted},
// delete waiter
testutil.MockResponse{
StatusCode: http.StatusGone,
},
)
},
ImportStateCheck: func(states []*terraform.InstanceState) error {
if len(states) != 1 {
return fmt.Errorf("expected exactly one state to be imported, got %d", len(states))
}
state := states[0]
if state.Attributes["instance_id"] != instanceId {
return fmt.Errorf("expected instance_id to be %s, got %s", instanceId, state.Attributes["instance_id"])
}
if state.Attributes["project_id"] != projectId {
return fmt.Errorf("expected project_id to be %s, got %s", projectId, state.Attributes["project_id"])
}
return nil
},
ImportState: true,
ImportStateId: fmt.Sprintf("%s,%s", projectId, instanceId),
ResourceName: "stackit_rabbitmq_instance.instance",
},
},
})
}

// Run apply for credentials and produce an error in the waiter. By erroring out state checks are not run in this step.
// The second step imports the resource and runs a state check to verify that the first step wrote the IDs, despite the error.
// When importing we append "-import" to the credential ID to verify that the import isn't overwriting the credential
// ID from the first step
func TestRabbitMQCredentialsSavesIDsOnError(t *testing.T) {
var (
projectId = uuid.NewString()
instanceId = uuid.NewString()
credentialId = uuid.NewString()
)
s := testutil.NewMockServer(t)
t.Cleanup(s.Server.Close)
tfConfig := fmt.Sprintf(`
provider "stackit" {
rabbitmq_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_rabbitmq_credential" "credential" {
project_id = "%s"
instance_id = "%s"
}
`, s.Server.URL, projectId, instanceId)
resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(
// initial post response
testutil.MockResponse{
ToJsonBody: rabbitmq.CredentialsResponse{
Id: utils.Ptr(credentialId),
},
},
// failing waiter
testutil.MockResponse{StatusCode: http.StatusInternalServerError},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating credential.*"),
},
{
PreConfig: func() {
s.Reset(
// read from import
testutil.MockResponse{
ToJsonBody: rabbitmq.CredentialsResponse{
Id: utils.Ptr(credentialId + "-import"),
Raw: &rabbitmq.RawCredentials{},
},
},
// delete
testutil.MockResponse{StatusCode: http.StatusAccepted},
// delete waiter
testutil.MockResponse{StatusCode: http.StatusGone},
)
},
ImportStateCheck: func(states []*terraform.InstanceState) error {
if len(states) != 1 {
return fmt.Errorf("expected exactly one state to be imported, got %d", len(states))
}
state := states[0]
if state.Attributes["instance_id"] != instanceId {
return fmt.Errorf("expected instance_id to be %s, got %s", instanceId, state.Attributes["instance_id"])
}
if state.Attributes["project_id"] != projectId {
return fmt.Errorf("expected project_id to be %s, got %s", projectId, state.Attributes["project_id"])
}
if state.Attributes["credential_id"] != credentialId {
return fmt.Errorf("expected credential_id to be %s, got %s", credentialId, state.Attributes["credential_id"])
}
return nil
},
ImportState: true,
ImportStateId: fmt.Sprintf("%s,%s,%s", projectId, instanceId, credentialId),
ResourceName: "stackit_rabbitmq_credential.credential",
},
},
})
}

func testAccCheckRabbitMQDestroy(s *terraform.State) error {
ctx := context.Background()
var client *rabbitmq.APIClient
Expand Down
67 changes: 67 additions & 0 deletions stackit/internal/testutil/mockserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package testutil

import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
)

type MockResponse struct {
StatusCode int
Description string
ToJsonBody any
}

var _ http.Handler = (*MockServer)(nil)

type MockServer struct {
mu sync.Mutex
nextResponse int
responses []MockResponse
Server *httptest.Server
t *testing.T
}

// NewMockServer creates a new simple mock server that returns `responses` in order for each request.
// Use the `Reset` method to reset the response order and set new responses.
func NewMockServer(t *testing.T, responses ...MockResponse) *MockServer {
mock := &MockServer{
nextResponse: 0,
responses: responses,
t: t,
}
mock.Server = httptest.NewServer(mock)
return mock
}

func (m *MockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.mu.Lock()
defer m.mu.Unlock()
if m.nextResponse >= len(m.responses) {
m.t.Fatalf("No more responses left in the mock server for request: %v", r)
}
next := m.responses[m.nextResponse]
m.nextResponse++
if next.ToJsonBody != nil {
bs, err := json.Marshal(next.ToJsonBody)
if err != nil {
m.t.Fatalf("Error marshaling response body: %v", err)
}
w.Header().Set("content-type", "application/json")
w.Write(bs) //nolint:errcheck //test will fail when this happens
}
status := next.StatusCode
if status == 0 {
status = http.StatusOK
}
w.WriteHeader(status)
}

func (m *MockServer) Reset(responses ...MockResponse) {
m.mu.Lock()
defer m.mu.Unlock()
m.nextResponse = 0
m.responses = responses
}
Loading