Skip to content

Commit 09bafa8

Browse files
authored
feat(observability): store ids immediately after provisioning (#1286)
relates to STACKITTPR-387
1 parent a109639 commit 09bafa8

6 files changed

Lines changed: 258 additions & 15 deletions

File tree

stackit/internal/services/observability/alertgroup/resource.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
1414
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1515
"github.com/hashicorp/terraform-plugin-framework/attr"
16-
"github.com/hashicorp/terraform-plugin-framework/path"
1716
"github.com/hashicorp/terraform-plugin-framework/resource"
1817
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1918
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
@@ -314,6 +313,16 @@ func (a *alertGroupResource) Create(ctx context.Context, req resource.CreateRequ
314313

315314
ctx = core.LogResponse(ctx)
316315

316+
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
317+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
318+
"project_id": projectId,
319+
"instance_id": instanceId,
320+
"name": alertGroupName,
321+
})
322+
if resp.Diagnostics.HasError() {
323+
return
324+
}
325+
317326
// all alert groups are returned. We have to search the map for the one corresponding to our name
318327
for _, alertGroup := range *createAlertGroupResp.Data {
319328
if model.Name.ValueString() != *alertGroup.Name {
@@ -430,9 +439,11 @@ func (a *alertGroupResource) ImportState(ctx context.Context, req resource.Impor
430439
return
431440
}
432441

433-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
434-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
435-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
442+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
443+
"project_id": idParts[0],
444+
"instance_id": idParts[1],
445+
"name": idParts[2],
446+
})
436447
tflog.Info(ctx, "Observability alert group state imported")
437448
}
438449

stackit/internal/services/observability/credential/resource.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,21 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
154154

155155
ctx = core.LogResponse(ctx)
156156

157+
if got == nil || got.Credentials == nil || got.Credentials.Username == nil {
158+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty username")
159+
return
160+
}
161+
username := *got.Credentials.Username
162+
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
163+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
164+
"project_id": projectId,
165+
"instance_id": instanceId,
166+
"username": username,
167+
})
168+
if resp.Diagnostics.HasError() {
169+
return
170+
}
171+
157172
err = mapFields(got.Credentials, &model)
158173
if err != nil {
159174
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err))

stackit/internal/services/observability/instance/resource.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
10021002
ctx = core.LogResponse(ctx)
10031003

10041004
instanceId := createResp.InstanceId
1005-
ctx = tflog.SetField(ctx, "instance_id", instanceId)
1005+
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
1006+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
1007+
"project_id": projectId,
1008+
"instance_id": instanceId,
1009+
})
1010+
if resp.Diagnostics.HasError() {
1011+
return
1012+
}
1013+
10061014
waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, *instanceId, projectId).WaitWithContext(ctx)
10071015
if err != nil {
10081016
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err))
@@ -1526,8 +1534,10 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS
15261534
return
15271535
}
15281536

1529-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
1530-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
1537+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
1538+
"project_id": idParts[0],
1539+
"instance_id": idParts[1],
1540+
})
15311541
tflog.Info(ctx, "Observability instance state imported")
15321542
}
15331543

stackit/internal/services/observability/log-alertgroup/resource.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
1414
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1515
"github.com/hashicorp/terraform-plugin-framework/attr"
16-
"github.com/hashicorp/terraform-plugin-framework/path"
1716
"github.com/hashicorp/terraform-plugin-framework/resource"
1817
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1918
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
@@ -262,6 +261,16 @@ func (l *logAlertGroupResource) Create(ctx context.Context, req resource.CreateR
262261

263262
ctx = core.LogResponse(ctx)
264263

264+
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
265+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
266+
"project_id": projectId,
267+
"instance_id": instanceId,
268+
"name": alertGroupName,
269+
})
270+
if resp.Diagnostics.HasError() {
271+
return
272+
}
273+
265274
// all log alert groups are returned. We have to search the map for the one corresponding to our name
266275
for _, alertGroup := range *createAlertGroupResp.Data {
267276
if model.Name.ValueString() != *alertGroup.Name {
@@ -378,9 +387,11 @@ func (l *logAlertGroupResource) ImportState(ctx context.Context, req resource.Im
378387
return
379388
}
380389

381-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
382-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
383-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
390+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
391+
"project_id": idParts[0],
392+
"instance_id": idParts[1],
393+
"name": idParts[2],
394+
})
384395
tflog.Info(ctx, "Observability log alert group state imported")
385396
}
386397

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package observability
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"regexp"
7+
"testing"
8+
9+
"github.com/google/uuid"
10+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
11+
"github.com/stackitcloud/stackit-sdk-go/core/utils"
12+
"github.com/stackitcloud/stackit-sdk-go/services/observability"
13+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
14+
)
15+
16+
func TestObservabilityInstanceSavesIDsOnError(t *testing.T) {
17+
projectId := uuid.NewString()
18+
planId := uuid.NewString()
19+
instanceId := uuid.NewString()
20+
const (
21+
region = "eu01"
22+
name = "observability-instance"
23+
planName = "Observability-Medium-EU01"
24+
)
25+
s := testutil.NewMockServer(t)
26+
defer s.Server.Close()
27+
tfConfig := fmt.Sprintf(`
28+
provider "stackit" {
29+
default_region = "%s"
30+
observability_custom_endpoint = "%s"
31+
service_account_token = "mock-server-needs-no-auth"
32+
}
33+
resource "stackit_observability_instance" "instance" {
34+
project_id = "%s"
35+
name = "%s"
36+
plan_name = "%s"
37+
}
38+
`, region, s.Server.URL, projectId, name, planName)
39+
40+
planList := testutil.MockResponse{
41+
Description: "plan list",
42+
ToJsonBody: observability.PlansResponse{
43+
Plans: utils.Ptr([]observability.Plan{
44+
{
45+
Name: utils.Ptr(planName),
46+
PlanId: utils.Ptr(planId),
47+
},
48+
}),
49+
},
50+
}
51+
52+
resource.UnitTest(t, resource.TestCase{
53+
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
54+
Steps: []resource.TestStep{
55+
{
56+
PreConfig: func() {
57+
s.Reset(
58+
planList,
59+
planList,
60+
planList,
61+
testutil.MockResponse{
62+
Description: "create instance",
63+
ToJsonBody: observability.CreateInstanceResponse{
64+
InstanceId: utils.Ptr(instanceId),
65+
},
66+
},
67+
testutil.MockResponse{
68+
Description: "failing waiter",
69+
StatusCode: http.StatusInternalServerError,
70+
},
71+
)
72+
},
73+
Config: tfConfig,
74+
ExpectError: regexp.MustCompile("Error creating instance.*"),
75+
},
76+
{
77+
PreConfig: func() {
78+
s.Reset(
79+
testutil.MockResponse{
80+
Description: "refresh",
81+
Handler: func(w http.ResponseWriter, req *http.Request) {
82+
expected := fmt.Sprintf("/v1/projects/%s/instances/%s", projectId, instanceId)
83+
if req.URL.Path != expected {
84+
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
85+
}
86+
w.WriteHeader(http.StatusInternalServerError)
87+
},
88+
},
89+
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
90+
testutil.MockResponse{
91+
Description: "delete waiter",
92+
ToJsonBody: observability.GetInstanceResponse{
93+
Id: utils.Ptr(instanceId),
94+
Status: observability.GETINSTANCERESPONSESTATUS_DELETE_SUCCEEDED.Ptr(),
95+
},
96+
},
97+
)
98+
},
99+
RefreshState: true,
100+
ExpectError: regexp.MustCompile("Error reading instance*"),
101+
},
102+
},
103+
})
104+
}
105+
106+
func TestObservabilityScrapeConfigSavesIDsOnError(t *testing.T) {
107+
projectId := uuid.NewString()
108+
instanceId := uuid.NewString()
109+
const (
110+
region = "eu01"
111+
name = "scrape-config"
112+
)
113+
s := testutil.NewMockServer(t)
114+
defer s.Server.Close()
115+
tfConfig := fmt.Sprintf(`
116+
provider "stackit" {
117+
default_region = "%s"
118+
observability_custom_endpoint = "%s"
119+
service_account_token = "mock-server-needs-no-auth"
120+
}
121+
resource "stackit_observability_scrapeconfig" "instance" {
122+
project_id = "%s"
123+
instance_id = "%s"
124+
name = "%s"
125+
metrics_path = "/my-metrics"
126+
targets = [
127+
{
128+
urls = ["url1", "urls2"]
129+
labels = {
130+
"url1" = "dev"
131+
}
132+
}
133+
]
134+
}
135+
`, region, s.Server.URL, projectId, instanceId, name)
136+
137+
resource.UnitTest(t, resource.TestCase{
138+
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
139+
Steps: []resource.TestStep{
140+
{
141+
PreConfig: func() {
142+
s.Reset(
143+
testutil.MockResponse{
144+
Description: "create scrape config",
145+
ToJsonBody: observability.ScrapeConfigsResponse{},
146+
},
147+
testutil.MockResponse{
148+
Description: "failing waiter",
149+
StatusCode: http.StatusInternalServerError,
150+
},
151+
)
152+
},
153+
Config: tfConfig,
154+
ExpectError: regexp.MustCompile("Error creating scrape config.*"),
155+
},
156+
{
157+
PreConfig: func() {
158+
s.Reset(
159+
testutil.MockResponse{
160+
Description: "refresh",
161+
Handler: func(w http.ResponseWriter, req *http.Request) {
162+
expected := fmt.Sprintf("/v1/projects/%s/instances/%s/scrapeconfigs/%s", projectId, instanceId, name)
163+
if req.URL.Path != expected {
164+
t.Errorf("expected request to %s, got %s", expected, req.URL.Path)
165+
}
166+
w.WriteHeader(http.StatusInternalServerError)
167+
},
168+
},
169+
testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
170+
testutil.MockResponse{
171+
Description: "delete waiter",
172+
ToJsonBody: observability.ListScrapeConfigsResponse{
173+
Data: utils.Ptr([]observability.Job{}),
174+
},
175+
},
176+
)
177+
},
178+
RefreshState: true,
179+
ExpectError: regexp.MustCompile("Error reading scrape config*"),
180+
},
181+
},
182+
})
183+
}

stackit/internal/services/observability/scrapeconfig/resource.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
1717
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1818
"github.com/hashicorp/terraform-plugin-framework/attr"
19-
"github.com/hashicorp/terraform-plugin-framework/path"
2019
"github.com/hashicorp/terraform-plugin-framework/resource"
2120
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2221
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
@@ -352,16 +351,28 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe
352351

353352
ctx = core.LogResponse(ctx)
354353

354+
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
355+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
356+
"project_id": projectId,
357+
"instance_id": instanceId,
358+
"name": scName,
359+
})
360+
if resp.Diagnostics.HasError() {
361+
return
362+
}
363+
355364
_, err = wait.CreateScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).WaitWithContext(ctx)
356365
if err != nil {
357366
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Scrape config creation waiting: %v", err))
358367
return
359368
}
369+
360370
got, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute()
361371
if err != nil {
362372
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API for updated data: %v", err))
363373
return
364374
}
375+
// Map response body to schema
365376
err = mapFields(ctx, got.Data, &model)
366377
if err != nil {
367378
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Processing API payload: %v", err))
@@ -545,9 +556,11 @@ func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.Imp
545556
return
546557
}
547558

548-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
549-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
550-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
559+
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
560+
"project_id": idParts[0],
561+
"instance_id": idParts[1],
562+
"name": idParts[2],
563+
})
551564
tflog.Info(ctx, "Observability scrape config state imported")
552565
}
553566

0 commit comments

Comments
 (0)