Skip to content

Commit 8bbc3e0

Browse files
Feat/stackittpr 393 store ids scf (#1245)
* fix(rabbitmq): Store IDs immediately after provisioning STACKITTPR-390 * chore(rabbitmq) write tests for saving IDs on create error * fix(lint) ignore write error in mockserver * fix(lint) add explanation to ignore comment * feat(scf) save IDs before calling the waiter in Create also fix rabbitmq tests STACKITTPR-393
1 parent 5291ccb commit 8bbc3e0

File tree

5 files changed

+135
-74
lines changed

5 files changed

+135
-74
lines changed

stackit/internal/services/rabbitmq/rabbitmq_acc_test.go

Lines changed: 26 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,7 @@ func TestAccRabbitMQResource(t *testing.T) {
246246
}
247247

248248
// Run apply for an instance and produce an error in the waiter. By erroring out state checks are not run in this step.
249-
// The second step imports the resource and runs a state check to verify that the first step wrote the IDs, despite the error.
250-
// When importing we append "-import" to the credential ID to verify that the import isn't overwriting the instance
251-
// ID from the first step
249+
// The second step refreshes the resource and verifies that the IDs are passed to the read function.
252250
func TestRabbitMQInstanceSavesIDsOnError(t *testing.T) {
253251
projectId := uuid.NewString()
254252
instanceId := uuid.NewString()
@@ -294,16 +292,15 @@ resource "stackit_rabbitmq_instance" "instance" {
294292
{
295293
PreConfig: func() {
296294
s.Reset(
297-
// respond to listing offerings
298295
offerings,
299-
// initial post response
300296
testutil.MockResponse{
297+
Description: "create",
301298
ToJsonBody: rabbitmq.CreateInstanceResponse{
302299
InstanceId: utils.Ptr(instanceId),
303300
},
304301
},
305-
// failing waiter
306302
testutil.MockResponse{
303+
Description: "create waiter",
307304
ToJsonBody: rabbitmq.Instance{
308305
Status: utils.Ptr(rabbitmq.INSTANCESTATUS_FAILED),
309306
},
@@ -316,49 +313,29 @@ resource "stackit_rabbitmq_instance" "instance" {
316313
{
317314
PreConfig: func() {
318315
s.Reset(
319-
// read from import
320316
testutil.MockResponse{
321-
ToJsonBody: rabbitmq.Instance{
322-
Status: utils.Ptr(rabbitmq.INSTANCESTATUS_ACTIVE),
323-
InstanceId: utils.Ptr(instanceId + "-import"),
324-
PlanId: utils.Ptr(planId),
317+
Description: "refresh",
318+
Handler: func(w http.ResponseWriter, req *http.Request) {
319+
expected := fmt.Sprintf("/v1/projects/%s/instances/%s", projectId, instanceId)
320+
if req.URL.Path != expected {
321+
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
322+
}
323+
w.WriteHeader(http.StatusInternalServerError)
325324
},
326325
},
327-
// list offerings in import
328-
offerings,
329-
// delete
330-
testutil.MockResponse{StatusCode: http.StatusAccepted},
331-
// delete waiter
332-
testutil.MockResponse{
333-
StatusCode: http.StatusGone,
334-
},
326+
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
327+
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusGone},
335328
)
336329
},
337-
ImportStateCheck: func(states []*terraform.InstanceState) error {
338-
if len(states) != 1 {
339-
return fmt.Errorf("expected exactly one state to be imported, got %d", len(states))
340-
}
341-
state := states[0]
342-
if state.Attributes["instance_id"] != instanceId {
343-
return fmt.Errorf("expected instance_id to be %s, got %s", instanceId, state.Attributes["instance_id"])
344-
}
345-
if state.Attributes["project_id"] != projectId {
346-
return fmt.Errorf("expected project_id to be %s, got %s", projectId, state.Attributes["project_id"])
347-
}
348-
return nil
349-
},
350-
ImportState: true,
351-
ImportStateId: fmt.Sprintf("%s,%s", projectId, instanceId),
352-
ResourceName: "stackit_rabbitmq_instance.instance",
330+
RefreshState: true,
331+
ExpectError: regexp.MustCompile("Error reading instance.*"),
353332
},
354333
},
355334
})
356335
}
357336

358337
// Run apply for credentials and produce an error in the waiter. By erroring out state checks are not run in this step.
359-
// The second step imports the resource and runs a state check to verify that the first step wrote the IDs, despite the error.
360-
// When importing we append "-import" to the credential ID to verify that the import isn't overwriting the credential
361-
// ID from the first step
338+
// The second step refreshes the resource and verifies that the IDs are passed to the read function.
362339
func TestRabbitMQCredentialsSavesIDsOnError(t *testing.T) {
363340
var (
364341
projectId = uuid.NewString()
@@ -400,38 +377,22 @@ resource "stackit_rabbitmq_credential" "credential" {
400377
{
401378
PreConfig: func() {
402379
s.Reset(
403-
// read from import
404380
testutil.MockResponse{
405-
ToJsonBody: rabbitmq.CredentialsResponse{
406-
Id: utils.Ptr(credentialId + "-import"),
407-
Raw: &rabbitmq.RawCredentials{},
381+
Description: "refresh",
382+
Handler: func(w http.ResponseWriter, req *http.Request) {
383+
expected := fmt.Sprintf("/v1/projects/%s/instances/%s/credentials/%s", projectId, instanceId, credentialId)
384+
if req.URL.Path != expected {
385+
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
386+
}
387+
w.WriteHeader(http.StatusInternalServerError)
408388
},
409389
},
410-
// delete
411-
testutil.MockResponse{StatusCode: http.StatusAccepted},
412-
// delete waiter
413-
testutil.MockResponse{StatusCode: http.StatusGone},
390+
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
391+
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusGone},
414392
)
415393
},
416-
ImportStateCheck: func(states []*terraform.InstanceState) error {
417-
if len(states) != 1 {
418-
return fmt.Errorf("expected exactly one state to be imported, got %d", len(states))
419-
}
420-
state := states[0]
421-
if state.Attributes["instance_id"] != instanceId {
422-
return fmt.Errorf("expected instance_id to be %s, got %s", instanceId, state.Attributes["instance_id"])
423-
}
424-
if state.Attributes["project_id"] != projectId {
425-
return fmt.Errorf("expected project_id to be %s, got %s", projectId, state.Attributes["project_id"])
426-
}
427-
if state.Attributes["credential_id"] != credentialId {
428-
return fmt.Errorf("expected credential_id to be %s, got %s", credentialId, state.Attributes["credential_id"])
429-
}
430-
return nil
431-
},
432-
ImportState: true,
433-
ImportStateId: fmt.Sprintf("%s,%s,%s", projectId, instanceId, credentialId),
434-
ResourceName: "stackit_rabbitmq_credential.credential",
394+
RefreshState: true,
395+
ExpectError: regexp.MustCompile("Error reading credential.*"),
435396
},
436397
},
437398
})

stackit/internal/services/scf/organization/resource.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"strings"
99

1010
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
11-
"github.com/hashicorp/terraform-plugin-framework/path"
1211
"github.com/hashicorp/terraform-plugin-framework/resource"
1312
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1413
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
@@ -260,8 +259,18 @@ func (s *scfOrganizationResource) Create(ctx context.Context, request resource.C
260259

261260
ctx = core.LogResponse(ctx)
262261

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

268+
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
269+
"project_id": projectId,
270+
"region": region,
271+
"org_id": orgId,
272+
})
273+
265274
// Apply the org quota if provided
266275
if quotaId != "" {
267276
applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload(
@@ -485,9 +494,11 @@ func (s *scfOrganizationResource) ImportState(ctx context.Context, request resou
485494
region := idParts[1]
486495
orgId := idParts[2]
487496
// Set the project id and organization id in the state
488-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
489-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
490-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
497+
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
498+
"project_id": projectId,
499+
"region": region,
500+
"org_id": orgId,
501+
})
491502
tflog.Info(ctx, "Scf organization state imported")
492503
}
493504

stackit/internal/services/scf/organizationmanager/resource.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"strings"
99

1010
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
11-
"github.com/hashicorp/terraform-plugin-framework/path"
1211
"github.com/hashicorp/terraform-plugin-framework/resource"
1312
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1413
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@@ -239,6 +238,17 @@ func (s *scfOrganizationManagerResource) Create(ctx context.Context, request res
239238

240239
ctx = core.LogResponse(ctx)
241240

241+
if scfOrgManagerCreateResponse.Guid == nil {
242+
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", "API response does not contain user id")
243+
}
244+
userId := *scfOrgManagerCreateResponse.Guid
245+
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
246+
"project_id": projectId,
247+
"region": region,
248+
"org_id": orgId,
249+
"user_id": userId,
250+
})
251+
242252
err = mapFieldsCreate(scfOrgManagerCreateResponse, &model)
243253
if err != nil {
244254
core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Mapping fields: %v", err))
@@ -360,10 +370,12 @@ func (s *scfOrganizationManagerResource) ImportState(ctx context.Context, reques
360370
orgId := idParts[2]
361371
userId := idParts[3]
362372
// Set the project id, region organization id and user id in the state
363-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
364-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...)
365-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...)
366-
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("user_id"), userId)...)
373+
ctx = utils.SetAndLogStateFields(ctx, &response.Diagnostics, &response.State, map[string]any{
374+
"project_id": projectId,
375+
"region": region,
376+
"org_id": orgId,
377+
"user_id": userId,
378+
})
367379
tflog.Info(ctx, "Scf organization manager state imported")
368380
}
369381

stackit/internal/services/scf/scf_acc_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import (
55
_ "embed"
66
"fmt"
77
"maps"
8+
"net/http"
9+
"regexp"
810
"strings"
911
"testing"
1012

13+
"github.com/google/uuid"
1114
"github.com/stackitcloud/stackit-sdk-go/services/scf"
1215

1316
"github.com/hashicorp/terraform-plugin-testing/config"
@@ -409,6 +412,71 @@ func TestAccScfOrgMax(t *testing.T) {
409412
})
410413
}
411414

415+
// Run apply and fail in the waiter. We expect that the IDs are saved in the state.
416+
// Verify this in the second step by refreshing and checking the IDs in the URL.
417+
func TestScfOrganizationSavesIDsOnError(t *testing.T) {
418+
var (
419+
projectId = uuid.NewString()
420+
guid = uuid.NewString()
421+
)
422+
const name = "scf-org-error-test"
423+
s := testutil.NewMockServer(t)
424+
defer s.Server.Close()
425+
tfConfig := fmt.Sprintf(`
426+
provider "stackit" {
427+
default_region = "eu01"
428+
scf_custom_endpoint = "%s"
429+
service_account_token = "mock-server-needs-no-auth"
430+
}
431+
432+
resource "stackit_scf_organization" "org" {
433+
project_id = "%s"
434+
name = "%s"
435+
}
436+
`, s.Server.URL, projectId, name)
437+
438+
resource.UnitTest(t, resource.TestCase{
439+
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
440+
Steps: []resource.TestStep{
441+
{
442+
PreConfig: func() {
443+
s.Reset(
444+
testutil.MockResponse{
445+
Description: "create",
446+
ToJsonBody: &scf.OrganizationCreateResponse{
447+
Guid: utils.Ptr(guid),
448+
},
449+
},
450+
testutil.MockResponse{Description: "create waiter", StatusCode: http.StatusNotFound},
451+
)
452+
},
453+
Config: tfConfig,
454+
ExpectError: regexp.MustCompile("Error creating scf organization.*"),
455+
},
456+
{
457+
PreConfig: func() {
458+
s.Reset(
459+
testutil.MockResponse{
460+
Description: "refresh",
461+
Handler: func(w http.ResponseWriter, req *http.Request) {
462+
expected := fmt.Sprintf("/v1/projects/%s/regions/%s/organizations/%s", projectId, region, guid)
463+
if req.URL.Path != expected {
464+
t.Errorf("Expected request to %s but got %s", expected, req.URL.Path)
465+
}
466+
w.WriteHeader(http.StatusInternalServerError)
467+
},
468+
},
469+
testutil.MockResponse{Description: "delete"},
470+
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound},
471+
)
472+
},
473+
RefreshState: true,
474+
ExpectError: regexp.MustCompile("Error reading scf organization.*"),
475+
},
476+
},
477+
})
478+
}
479+
412480
func testAccCheckScfOrganizationDestroy(s *terraform.State) error {
413481
ctx := context.Background()
414482
var client *scf.APIClient

stackit/internal/testutil/mockserver.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import (
88
"testing"
99
)
1010

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

1722
var _ http.Handler = (*MockServer)(nil)
@@ -44,6 +49,10 @@ func (m *MockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
4449
}
4550
next := m.responses[m.nextResponse]
4651
m.nextResponse++
52+
if next.Handler != nil {
53+
next.Handler(w, r)
54+
return
55+
}
4756
if next.ToJsonBody != nil {
4857
bs, err := json.Marshal(next.ToJsonBody)
4958
if err != nil {

0 commit comments

Comments
 (0)