Skip to content
Open
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
91 changes: 26 additions & 65 deletions stackit/internal/services/rabbitmq/rabbitmq_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,7 @@ 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
// The second step refreshes the resource and verifies that the IDs are passed to the read function.
func TestRabbitMQInstanceSavesIDsOnError(t *testing.T) {
projectId := uuid.NewString()
instanceId := uuid.NewString()
Expand Down Expand Up @@ -294,16 +292,15 @@ resource "stackit_rabbitmq_instance" "instance" {
{
PreConfig: func() {
s.Reset(
// respond to listing offerings
offerings,
// initial post response
testutil.MockResponse{
Description: "create",
ToJsonBody: rabbitmq.CreateInstanceResponse{
InstanceId: utils.Ptr(instanceId),
},
},
// failing waiter
testutil.MockResponse{
Description: "create waiter",
ToJsonBody: rabbitmq.Instance{
Status: utils.Ptr(rabbitmq.INSTANCESTATUS_FAILED),
},
Expand All @@ -316,49 +313,29 @@ resource "stackit_rabbitmq_instance" "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),
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/instances/%s", projectId, instanceId)
if req.URL.Path != expected {
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
// list offerings in import
offerings,
// delete
testutil.MockResponse{StatusCode: http.StatusAccepted},
// delete waiter
testutil.MockResponse{
StatusCode: http.StatusGone,
},
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
testutil.MockResponse{Description: "delete waiter", 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",
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading 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
// The second step refreshes the resource and verifies that the IDs are passed to the read function.
func TestRabbitMQCredentialsSavesIDsOnError(t *testing.T) {
var (
projectId = uuid.NewString()
Expand Down Expand Up @@ -400,38 +377,22 @@ resource "stackit_rabbitmq_credential" "credential" {
{
PreConfig: func() {
s.Reset(
// read from import
testutil.MockResponse{
ToJsonBody: rabbitmq.CredentialsResponse{
Id: utils.Ptr(credentialId + "-import"),
Raw: &rabbitmq.RawCredentials{},
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/instances/%s/credentials/%s", projectId, instanceId, credentialId)
if req.URL.Path != expected {
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
// delete
testutil.MockResponse{StatusCode: http.StatusAccepted},
// delete waiter
testutil.MockResponse{StatusCode: http.StatusGone},
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
testutil.MockResponse{Description: "delete waiter", 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",
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading credential.*"),
},
},
})
Expand Down
19 changes: 15 additions & 4 deletions stackit/internal/services/scf/organization/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"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/boolplanmodifier"
Expand Down Expand Up @@ -260,8 +259,18 @@ func (s *scfOrganizationResource) Create(ctx context.Context, request resource.C

ctx = core.LogResponse(ctx)

if scfOrgCreateResponse.Guid == nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", "API response did not include org ID")
return
}
orgId := *scfOrgCreateResponse.Guid

ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
"project_id": projectId,
"region": region,
"org_id": orgId,
})

// Apply the org quota if provided
if quotaId != "" {
applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload(
Expand Down Expand Up @@ -485,9 +494,11 @@ func (s *scfOrganizationResource) ImportState(ctx context.Context, request resou
region := idParts[1]
orgId := idParts[2]
// Set the project id and organization id in the state
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
"project_id": projectId,
"region": region,
"org_id": orgId,
})
tflog.Info(ctx, "Scf organization state imported")
}

Expand Down
22 changes: 17 additions & 5 deletions stackit/internal/services/scf/organizationmanager/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"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 @@ -239,6 +238,17 @@ func (s *scfOrganizationManagerResource) Create(ctx context.Context, request res

ctx = core.LogResponse(ctx)

if scfOrgManagerCreateResponse.Guid == nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", "API response does not contain user id")
}
userId := *scfOrgManagerCreateResponse.Guid
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
"project_id": projectId,
"region": region,
"org_id": orgId,
"user_id": userId,
})

err = mapFieldsCreate(scfOrgManagerCreateResponse, &model)
if err != nil {
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Mapping fields: %v", err))
Expand Down Expand Up @@ -360,10 +370,12 @@ func (s *scfOrganizationManagerResource) ImportState(ctx context.Context, reques
orgId := idParts[2]
userId := idParts[3]
// Set the project id, region organization id and user id in the state
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("user_id"), userId)...)
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
"project_id": projectId,
"region": region,
"org_id": orgId,
"user_id": userId,
})
tflog.Info(ctx, "Scf organization manager state imported")
}

Expand Down
68 changes: 68 additions & 0 deletions stackit/internal/services/scf/scf_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
_ "embed"
"fmt"
"maps"
"net/http"
"regexp"
"strings"
"testing"

"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/scf"

"github.com/hashicorp/terraform-plugin-testing/config"
Expand Down Expand Up @@ -409,6 +412,71 @@ func TestAccScfOrgMax(t *testing.T) {
})
}

// Run apply and fail in the waiter. We expect that the IDs are saved in the state.
// Verify this in the second step by refreshing and checking the IDs in the URL.
func TestScfOrganizationSavesIDsOnError(t *testing.T) {
var (
projectId = uuid.NewString()
guid = uuid.NewString()
)
const name = "scf-org-error-test"
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
default_region = "eu01"
scf_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_scf_organization" "org" {
project_id = "%s"
name = "%s"
}
`, s.Server.URL, projectId, name)

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "create",
ToJsonBody: &scf.OrganizationCreateResponse{
Guid: utils.Ptr(guid),
},
},
testutil.MockResponse{Description: "create waiter", StatusCode: http.StatusNotFound},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating scf organization.*"),
},
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/regions/%s/organizations/%s", projectId, region, guid)
if req.URL.Path != expected {
t.Errorf("Expected request to %s but got %s", expected, req.URL.Path)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
testutil.MockResponse{Description: "delete"},
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound},
)
},
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading scf organization.*"),
},
},
})
}

func testAccCheckScfOrganizationDestroy(s *terraform.State) error {
ctx := context.Background()
var client *scf.APIClient
Expand Down
9 changes: 9 additions & 0 deletions stackit/internal/testutil/mockserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import (
"testing"
)

// MockResponse represents a single response that the MockServer will return for a request.
// If `Handler` is set, it will be used to handle the request and the other fields will be ignored.
// If `ToJsonBody` is set, it will be marshaled to JSON and returned as the response body with content-type application/json.
// If `StatusCode` is set, it will be used as the response status code. Otherwise, http.StatusOK will be used.
type MockResponse struct {
StatusCode int
Description string
ToJsonBody any
Handler http.HandlerFunc
}

var _ http.Handler = (*MockServer)(nil)
Expand Down Expand Up @@ -44,6 +49,10 @@ func (m *MockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
next := m.responses[m.nextResponse]
m.nextResponse++
if next.Handler != nil {
next.Handler(w, r)
return
}
if next.ToJsonBody != nil {
bs, err := json.Marshal(next.ToJsonBody)
if err != nil {
Expand Down
Loading