Skip to content

Commit 9ed26b7

Browse files
authored
feat(mongodb): add mongodb instance snapshot action (#3494)
* feat: add mongodb instance snapshot action * feat: make expires_at optional in MongoDB snapshot action and add audit trail verification * feat: replace audit trail verification with direct snapshot check for MongoDB action * feat: compress MongoDB snapshot action test cassette * fix: correct linting issues (wsl_v5) and regenerate docs * fix: move expires_at to Optional section in docs * fix: apply code review feedback for MongoDB snapshot action * fix: remove unnecessary validator and improve default snapshot name * fix: correct import order in framework.go * fix: remove blank line between import groups in framework.go
1 parent 7fa5dbf commit 9ed26b7

File tree

6 files changed

+1317
-0
lines changed

6 files changed

+1317
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
subcategory: "MongoDB"
3+
page_title: "Scaleway: scaleway_mongodb_instance_snapshot_action"
4+
---
5+
6+
# scaleway_mongodb_instance_snapshot_action (Action)
7+
8+
<!-- action schema generated by tfplugindocs -->
9+
## Schema
10+
11+
### Required
12+
13+
- `instance_id` (String) MongoDB instance ID to snapshot. Can be a plain UUID or a regional ID.
14+
15+
### Optional
16+
17+
- `expires_at` (String) Expiration date of the snapshot in RFC3339 format (ISO 8601). If not set, the snapshot will not expire.
18+
- `name` (String) Name of the snapshot. If not set, a name will be generated.
19+
- `region` (String) Region of the MongoDB instance. If not set, the region is derived from the instance_id when possible or from the provider configuration.
20+
- `wait` (Boolean) Wait for the snapshot to reach a terminal state before returning.
21+
22+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package mongodb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/action"
9+
"github.com/hashicorp/terraform-plugin-framework/action/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
mongodb "github.com/scaleway/scaleway-sdk-go/api/mongodb/v1"
12+
"github.com/scaleway/scaleway-sdk-go/scw"
13+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
14+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
15+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/meta"
16+
)
17+
18+
var (
19+
_ action.Action = (*InstanceSnapshotAction)(nil)
20+
_ action.ActionWithConfigure = (*InstanceSnapshotAction)(nil)
21+
)
22+
23+
// InstanceSnapshotAction creates a snapshot for a MongoDB instance.
24+
type InstanceSnapshotAction struct {
25+
mongodbAPI *mongodb.API
26+
meta *meta.Meta
27+
}
28+
29+
func (a *InstanceSnapshotAction) Configure(_ context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) {
30+
if req.ProviderData == nil {
31+
return
32+
}
33+
34+
m, ok := req.ProviderData.(*meta.Meta)
35+
if !ok {
36+
resp.Diagnostics.AddError(
37+
"Unexpected Action Configure Type",
38+
fmt.Sprintf("Expected *meta.Meta, got: %T. Please report this issue to the provider developers.", req.ProviderData),
39+
)
40+
41+
return
42+
}
43+
44+
a.meta = m
45+
a.mongodbAPI = newAPI(m)
46+
}
47+
48+
func (a *InstanceSnapshotAction) Metadata(_ context.Context, req action.MetadataRequest, resp *action.MetadataResponse) {
49+
resp.TypeName = req.ProviderTypeName + "_mongodb_instance_snapshot_action"
50+
}
51+
52+
type InstanceSnapshotActionModel struct {
53+
InstanceID types.String `tfsdk:"instance_id"`
54+
Region types.String `tfsdk:"region"`
55+
Name types.String `tfsdk:"name"`
56+
ExpiresAt types.String `tfsdk:"expires_at"`
57+
Wait types.Bool `tfsdk:"wait"`
58+
}
59+
60+
// NewInstanceSnapshotAction returns a new MongoDB instance snapshot action.
61+
func NewInstanceSnapshotAction() action.Action {
62+
return &InstanceSnapshotAction{}
63+
}
64+
65+
func (a *InstanceSnapshotAction) Schema(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) {
66+
resp.Schema = schema.Schema{
67+
Attributes: map[string]schema.Attribute{
68+
"instance_id": schema.StringAttribute{
69+
Required: true,
70+
Description: "MongoDB instance ID to snapshot. Can be a plain UUID or a regional ID.",
71+
},
72+
"region": schema.StringAttribute{
73+
Optional: true,
74+
Description: "Region of the MongoDB instance. If not set, the region is derived from the instance_id when possible or from the provider configuration.",
75+
},
76+
"name": schema.StringAttribute{
77+
Optional: true,
78+
Description: "Name of the snapshot. If not set, a name will be generated.",
79+
},
80+
"expires_at": schema.StringAttribute{
81+
Optional: true,
82+
Description: "Expiration date of the snapshot in RFC3339 format (ISO 8601). If not set, the snapshot will not expire.",
83+
},
84+
"wait": schema.BoolAttribute{
85+
Optional: true,
86+
Description: "Wait for the snapshot to reach a terminal state before returning.",
87+
},
88+
},
89+
}
90+
}
91+
92+
func (a *InstanceSnapshotAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
93+
var data InstanceSnapshotActionModel
94+
95+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
96+
97+
if resp.Diagnostics.HasError() {
98+
return
99+
}
100+
101+
if a.mongodbAPI == nil {
102+
resp.Diagnostics.AddError(
103+
"Unconfigured mongodbAPI",
104+
"The action was not properly configured. The Scaleway client is missing. "+
105+
"This is usually a bug in the provider. Please report it to the maintainers.",
106+
)
107+
108+
return
109+
}
110+
111+
if data.InstanceID.IsNull() || data.InstanceID.IsUnknown() || data.InstanceID.ValueString() == "" {
112+
resp.Diagnostics.AddError(
113+
"Missing instance_id",
114+
"The instance_id attribute is required to create a MongoDB snapshot.",
115+
)
116+
117+
return
118+
}
119+
120+
instanceID := locality.ExpandID(data.InstanceID.ValueString())
121+
122+
var region scw.Region
123+
124+
if !data.Region.IsNull() && !data.Region.IsUnknown() && data.Region.ValueString() != "" {
125+
region = scw.Region(data.Region.ValueString())
126+
} else {
127+
// Try to derive region from the instance_id if it is a regional ID.
128+
if derivedRegion, id, parseErr := regional.ParseID(data.InstanceID.ValueString()); parseErr == nil {
129+
region = derivedRegion
130+
instanceID = id
131+
} else if a.meta != nil {
132+
// Fallback to provider default region
133+
defaultRegion, exists := a.meta.ScwClient().GetDefaultRegion()
134+
if !exists {
135+
resp.Diagnostics.AddError(
136+
"Unable to determine region",
137+
"Failed to get default region from provider configuration. Please set the region attribute, use a regional instance_id, or configure a default region in the provider.",
138+
)
139+
140+
return
141+
}
142+
143+
region = defaultRegion
144+
}
145+
}
146+
147+
if region == "" {
148+
resp.Diagnostics.AddError(
149+
"Missing region",
150+
"Could not determine region for MongoDB snapshot. Please set the region attribute, use a regional instance_id, or configure a default region in the provider.",
151+
)
152+
153+
return
154+
}
155+
156+
snapshotName := data.Name.ValueString()
157+
if snapshotName == "" {
158+
snapshotName = "tf-mongodb-snapshot-action"
159+
}
160+
161+
var expirationTime *time.Time
162+
163+
if !data.ExpiresAt.IsNull() && !data.ExpiresAt.IsUnknown() && data.ExpiresAt.ValueString() != "" {
164+
expirationRaw := data.ExpiresAt.ValueString()
165+
166+
parsedTime, err := time.Parse(time.RFC3339, expirationRaw)
167+
if err != nil {
168+
resp.Diagnostics.AddError(
169+
"Invalid expires_at value",
170+
fmt.Sprintf("The expires_at attribute must be a valid RFC3339 timestamp. Got %q: %s", expirationRaw, err),
171+
)
172+
173+
return
174+
}
175+
176+
expirationTime = &parsedTime
177+
}
178+
179+
createReq := &mongodb.CreateSnapshotRequest{
180+
InstanceID: instanceID,
181+
Name: snapshotName,
182+
ExpiresAt: expirationTime,
183+
}
184+
185+
if region != "" {
186+
createReq.Region = region
187+
}
188+
189+
snapshot, err := a.mongodbAPI.CreateSnapshot(createReq, scw.WithContext(ctx))
190+
if err != nil {
191+
resp.Diagnostics.AddError(
192+
"Error executing MongoDB CreateSnapshot action",
193+
fmt.Sprintf("Failed to create snapshot for instance %s: %s", instanceID, err),
194+
)
195+
196+
return
197+
}
198+
199+
if data.Wait.ValueBool() {
200+
waitRegion := snapshot.Region
201+
if waitRegion == "" && region != "" {
202+
waitRegion = region
203+
}
204+
205+
if waitRegion == "" {
206+
resp.Diagnostics.AddError(
207+
"Missing region for wait operation",
208+
"Could not determine region to wait for MongoDB snapshot completion.",
209+
)
210+
211+
return
212+
}
213+
214+
_, err = waitForSnapshot(ctx, a.mongodbAPI, waitRegion, instanceID, snapshot.ID, defaultMongodbSnapshotTimeout)
215+
if err != nil {
216+
resp.Diagnostics.AddError(
217+
"Error waiting for MongoDB snapshot completion",
218+
fmt.Sprintf("Snapshot %s for instance %s did not reach a terminal state: %s", snapshot.ID, instanceID, err),
219+
)
220+
221+
return
222+
}
223+
}
224+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package mongodb_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
9+
"github.com/hashicorp/terraform-plugin-testing/terraform"
10+
mongodbSDK "github.com/scaleway/scaleway-sdk-go/api/mongodb/v1"
11+
"github.com/scaleway/scaleway-sdk-go/scw"
12+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest"
13+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
14+
)
15+
16+
func TestAccActionMongoDBInstanceSnapshot_Basic(t *testing.T) {
17+
if acctest.IsRunningOpenTofu() {
18+
t.Skip("Skipping TestAccActionMongoDBInstanceSnapshot_Basic because actions are not yet supported on OpenTofu")
19+
}
20+
21+
tt := acctest.NewTestTools(t)
22+
defer tt.Cleanup()
23+
24+
resource.ParallelTest(t, resource.TestCase{
25+
ProtoV6ProviderFactories: tt.ProviderFactories,
26+
Steps: []resource.TestStep{
27+
{
28+
Config: `
29+
resource "scaleway_mongodb_instance" "main" {
30+
name = "test-mongodb-action-snapshot"
31+
version = "7.0"
32+
node_type = "MGDB-PLAY2-NANO"
33+
node_number = 1
34+
user_name = "my_initial_user"
35+
password = "thiZ_is_v&ry_s3cret"
36+
37+
lifecycle {
38+
action_trigger {
39+
events = [after_create]
40+
actions = [action.scaleway_mongodb_instance_snapshot_action.main]
41+
}
42+
}
43+
}
44+
45+
action "scaleway_mongodb_instance_snapshot_action" "main" {
46+
config {
47+
instance_id = scaleway_mongodb_instance.main.id
48+
name = "tf-acc-mongodb-instance-snapshot-action"
49+
expires_at = "2026-11-01T00:00:00Z"
50+
wait = true
51+
}
52+
}
53+
`,
54+
},
55+
{
56+
Config: `
57+
resource "scaleway_mongodb_instance" "main" {
58+
name = "test-mongodb-action-snapshot"
59+
version = "7.0"
60+
node_type = "MGDB-PLAY2-NANO"
61+
node_number = 1
62+
user_name = "my_initial_user"
63+
password = "thiZ_is_v&ry_s3cret"
64+
65+
lifecycle {
66+
action_trigger {
67+
events = [after_create]
68+
actions = [action.scaleway_mongodb_instance_snapshot_action.main]
69+
}
70+
}
71+
}
72+
73+
action "scaleway_mongodb_instance_snapshot_action" "main" {
74+
config {
75+
instance_id = scaleway_mongodb_instance.main.id
76+
name = "tf-acc-mongodb-instance-snapshot-action"
77+
expires_at = "2026-11-01T00:00:00Z"
78+
wait = true
79+
}
80+
}
81+
`,
82+
Check: resource.ComposeTestCheckFunc(
83+
isSnapshotCreated(tt, "scaleway_mongodb_instance.main", "tf-acc-mongodb-instance-snapshot-action"),
84+
),
85+
},
86+
},
87+
})
88+
}
89+
90+
func isSnapshotCreated(tt *acctest.TestTools, instanceResourceName, snapshotName string) resource.TestCheckFunc {
91+
return func(state *terraform.State) error {
92+
rs, ok := state.RootModule().Resources[instanceResourceName]
93+
if !ok {
94+
return fmt.Errorf("resource not found: %s", instanceResourceName)
95+
}
96+
97+
instanceID := rs.Primary.ID
98+
99+
region, id, err := regional.ParseID(instanceID)
100+
if err != nil {
101+
return fmt.Errorf("failed to parse instance ID: %w", err)
102+
}
103+
104+
api := mongodbSDK.NewAPI(tt.Meta.ScwClient())
105+
106+
snapshots, err := api.ListSnapshots(&mongodbSDK.ListSnapshotsRequest{
107+
Region: region,
108+
InstanceID: &id,
109+
}, scw.WithAllPages(), scw.WithContext(context.Background()))
110+
if err != nil {
111+
return fmt.Errorf("failed to list snapshots: %w", err)
112+
}
113+
114+
for _, snapshot := range snapshots.Snapshots {
115+
if snapshot.Name == snapshotName {
116+
return nil
117+
}
118+
}
119+
120+
return fmt.Errorf("snapshot with name %q not found for instance %s", snapshotName, instanceID)
121+
}
122+
}

0 commit comments

Comments
 (0)