Skip to content

feat(rdb): creating instance from snapshot #2872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Feb 13, 2025
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
101 changes: 101 additions & 0 deletions docs/resources/rdb_snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
subcategory: "Databases"
page_title: "Scaleway: scaleway_rdb_snapshot"
---

# Resource: scaleway_rdb_snapshot

Creates and manages Scaleway RDB (Relational Database) Snapshots.
Snapshots are point-in-time backups of a database instance that can be used for recovery or duplication.
For more information, refer to [the API documentation](https://www.scaleway.com/en/developers/api/managed-database-postgre-mysql/).

## Example Usage

### Example Basic Snapshot

```terraform
resource "scaleway_rdb_instance" "main" {
name = "test-rdb-instance"
node_type = "db-dev-s"
engine = "PostgreSQL-15"
is_ha_cluster = false
disable_backup = true
user_name = "my_initial_user"
password = "thiZ_is_v&ry_s3cret"
tags = ["terraform-test", "scaleway_rdb_instance", "minimal"]
volume_type = "bssd"
volume_size_in_gb = 10
}

resource "scaleway_rdb_snapshot" "test" {
name = "initial-snapshot"
instance_id = scaleway_rdb_instance.main.id
depends_on = [scaleway_rdb_instance.main]
}
```

### Example with Expiration

```terraform
resource "scaleway_rdb_snapshot" "snapshot_with_expiration" {
name = "snapshot-with-expiration"
instance_id = scaleway_rdb_instance.main.id
expires_at = "2025-01-31T00:00:00Z"
}
```

### Example with Multiple Snapshots

```terraform
resource "scaleway_rdb_snapshot" "snapshot_a" {
name = "snapshot_a"
instance_id = scaleway_rdb_instance.main.id
depends_on = [scaleway_rdb_instance.main]
}

resource "scaleway_rdb_snapshot" "snapshot_b" {
name = "snapshot_b"
instance_id = scaleway_rdb_instance.main.id
expires_at = "2025-02-07T00:00:00Z"
depends_on = [scaleway_rdb_instance.main]
}
```

## Argument Reference

The following arguments are supported:

- `name` - (Required) The name of the snapshot.
- `instance_id` - (Required) The UUID of the database instance for which the snapshot is created.
- `snapshot_id` - (Optional, ForceNew) The ID of an existing snapshot. This allows creating an instance from a specific snapshot ID. Conflicts with `engine`.
- `expires_at` - (Optional) Expiration date of the snapshot in ISO 8601 format (e.g., `2025-01-31T00:00:00Z`). If not set, the snapshot will not expire automatically.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

- `id` - The unique ID of the snapshot.
- `created_at` - The timestamp when the snapshot was created, in ISO 8601 format.
- `updated_at` - The timestamp when the snapshot was last updated, in ISO 8601 format.
- `status` - The current status of the snapshot (e.g., `ready`, `creating`, `error`).
- `size` - The size of the snapshot in bytes.
- `node_type` - The type of the database instance for which the snapshot was created.
- `volume_type` - The type of volume used by the snapshot.

## Attributes Reference

- `region` - The region where the snapshot is stored. Defaults to the region set in the provider configuration.

## Import

RDB Snapshots can be imported using the `{region}/{snapshot_id}` format.

## Limitations

- Snapshots are tied to the database instance and region where they are created.
- Expired snapshots are automatically deleted and cannot be restored.

## Notes

- Ensure the `instance_id` corresponds to an existing database instance.
- Use the `depends_on` argument when creating snapshots right after creating an instance to ensure proper dependency management.
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ func Provider(config *Config) plugin.ProviderFunc {
"scaleway_rdb_privilege": rdb.ResourcePrivilege(),
"scaleway_rdb_read_replica": rdb.ResourceReadReplica(),
"scaleway_rdb_user": rdb.ResourceUser(),
"scaleway_rdb_snapshot": rdb.ResourceSnapshot(),
"scaleway_redis_cluster": redis.ResourceCluster(),
"scaleway_registry_namespace": registry.ResourceNamespace(),
"scaleway_sdb_sql_database": sdb.ResourceDatabase(),
Expand Down
182 changes: 132 additions & 50 deletions internal/services/rdb/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,23 @@ func ResourceInstance() *schema.Resource {
},
"engine": {
Type: schema.TypeString,
Required: true,
Optional: true,
Computed: true,
ForceNew: true,
Description: "Database's engine version id",
DiffSuppressFunc: dsf.IgnoreCase,
ConflictsWith: []string{
"snapshot_id",
},
},
"snapshot_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "ID of an existing snapshot to create a new instance from. This allows restoring a database instance to the state captured in the specified snapshot. Conflicts with the `engine` attribute.",
ConflictsWith: []string{
"engine",
},
},
"is_ha_cluster": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -318,76 +331,145 @@ func ResourceInstance() *schema.Resource {
}
}

//gocyclo:ignore
func ResourceRdbInstanceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
rdbAPI, region, err := newAPIWithRegion(d, m)
if err != nil {
return diag.FromErr(err)
}

createReq := &rdb.CreateInstanceRequest{
Region: region,
ProjectID: types.ExpandStringPtr(d.Get("project_id")),
Name: types.ExpandOrGenerateString(d.Get("name"), "rdb"),
NodeType: d.Get("node_type").(string),
Engine: d.Get("engine").(string),
IsHaCluster: d.Get("is_ha_cluster").(bool),
DisableBackup: d.Get("disable_backup").(bool),
UserName: d.Get("user_name").(string),
Password: d.Get("password").(string),
VolumeType: rdb.VolumeType(d.Get("volume_type").(string)),
Encryption: &rdb.EncryptionAtRest{
Enabled: d.Get("encryption_at_rest").(bool),
},
}
var id string

if initSettings, ok := d.GetOk("init_settings"); ok {
createReq.InitSettings = expandInstanceSettings(initSettings)
}
if regionalSnapshotID, ok := d.GetOk("snapshot_id"); ok {
haCluster := d.Get("is_ha_cluster").(bool)
nodeType := d.Get("node_type").(string)

rawTag, tagExist := d.GetOk("tags")
if tagExist {
createReq.Tags = types.ExpandStrings(rawTag)
}
_, snapshotID, err := regional.ParseID(regionalSnapshotID.(string))
if err != nil {
return diag.FromErr(err)
}

// Init Endpoints
if pn, pnExist := d.GetOk("private_network"); pnExist {
ipamConfig, staticConfig := getIPConfigCreate(d, "ip_net")
createReqFromSnapshot := &rdb.CreateInstanceFromSnapshotRequest{
SnapshotID: snapshotID,
Region: region,
InstanceName: types.ExpandOrGenerateString(d.Get("name"), "rdb"),
IsHaCluster: &haCluster,
NodeType: &nodeType,
}

var diags diag.Diagnostics
res, err := rdbAPI.CreateInstanceFromSnapshot(createReqFromSnapshot, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

createReq.InitEndpoints, diags = expandPrivateNetwork(pn, pnExist, ipamConfig, staticConfig)
if diags.HasError() {
return diags
_, err = waitForRDBInstance(ctx, rdbAPI, region, res.ID, d.Timeout(schema.TimeoutCreate))
if err != nil {
return diag.FromErr(err)
}

for _, warning := range diags {
tflog.Warn(ctx, warning.Detail)
rawTag, tagExist := d.GetOk("tags")
if tagExist {
updateReq := &rdb.UpdateInstanceRequest{
Region: region,
InstanceID: res.ID,
}
tags := types.ExpandStrings(rawTag)
updateReq.Tags = &tags

_, err = rdbAPI.UpdateInstance(updateReq, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
}
}

if _, lbExists := d.GetOk("load_balancer"); lbExists {
createReq.InitEndpoints = append(createReq.InitEndpoints, expandLoadBalancer())
}
d.SetId(regional.NewIDString(region, res.ID))
id = res.ID
} else {
createReq := &rdb.CreateInstanceRequest{
Region: region,
ProjectID: types.ExpandStringPtr(d.Get("project_id")),
Name: types.ExpandOrGenerateString(d.Get("name"), "rdb"),
NodeType: d.Get("node_type").(string),
Engine: d.Get("engine").(string),
IsHaCluster: d.Get("is_ha_cluster").(bool),
DisableBackup: d.Get("disable_backup").(bool),
UserName: d.Get("user_name").(string),
Password: d.Get("password").(string),
VolumeType: rdb.VolumeType(d.Get("volume_type").(string)),
Encryption: &rdb.EncryptionAtRest{
Enabled: d.Get("encryption_at_rest").(bool),
},
}

if size, ok := d.GetOk("volume_size_in_gb"); ok {
if createReq.VolumeType == rdb.VolumeTypeLssd {
return diag.FromErr(fmt.Errorf("volume_size_in_gb should not be used with volume_type %s", rdb.VolumeTypeLssd.String()))
if initSettings, ok := d.GetOk("init_settings"); ok {
createReq.InitSettings = expandInstanceSettings(initSettings)
}

createReq.VolumeSize = scw.Size(uint64(size.(int)) * uint64(scw.GB))
}
rawTag, tagExist := d.GetOk("tags")
if tagExist {
createReq.Tags = types.ExpandStrings(rawTag)
}

res, err := rdbAPI.CreateInstance(createReq, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
// Init Endpoints
if pn, pnExist := d.GetOk("private_network"); pnExist {
ipamConfig, staticConfig := getIPConfigCreate(d, "ip_net")

var diags diag.Diagnostics

createReq.InitEndpoints, diags = expandPrivateNetwork(pn, pnExist, ipamConfig, staticConfig)
if diags.HasError() {
return diags
}

for _, warning := range diags {
tflog.Warn(ctx, warning.Detail)
}
}

if _, lbExists := d.GetOk("load_balancer"); lbExists {
createReq.InitEndpoints = append(createReq.InitEndpoints, expandLoadBalancer())
}
// Init Endpoints
if pn, pnExist := d.GetOk("private_network"); pnExist {
ipamConfig, staticConfig := getIPConfigCreate(d, "ip_net")

var diags diag.Diagnostics

createReq.InitEndpoints, diags = expandPrivateNetwork(pn, pnExist, ipamConfig, staticConfig)
if diags.HasError() {
return diags
}

d.SetId(regional.NewIDString(region, res.ID))
for _, warning := range diags {
tflog.Warn(ctx, warning.Detail)
}
}

if _, lbExists := d.GetOk("load_balancer"); lbExists {
createReq.InitEndpoints = append(createReq.InitEndpoints, expandLoadBalancer())
}

if size, ok := d.GetOk("volume_size_in_gb"); ok {
if createReq.VolumeType == rdb.VolumeTypeLssd {
return diag.FromErr(fmt.Errorf("volume_size_in_gb should not be used with volume_type %s", rdb.VolumeTypeLssd.String()))
}

createReq.VolumeSize = scw.Size(uint64(size.(int)) * uint64(scw.GB))
}

res, err := rdbAPI.CreateInstance(createReq, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

d.SetId(regional.NewIDString(region, res.ID))
id = res.ID
}

mustUpdate := false
updateReq := &rdb.UpdateInstanceRequest{
Region: region,
InstanceID: res.ID,
InstanceID: id,
}
// Configure Schedule Backup
// BackupScheduleFrequency and BackupScheduleRetention can only configure after instance creation
Expand All @@ -413,7 +495,7 @@ func ResourceRdbInstanceCreate(ctx context.Context, d *schema.ResourceData, m in
}

if mustUpdate {
_, err = waitForRDBInstance(ctx, rdbAPI, region, res.ID, d.Timeout(schema.TimeoutCreate))
_, err = waitForRDBInstance(ctx, rdbAPI, region, id, d.Timeout(schema.TimeoutCreate))
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -425,12 +507,12 @@ func ResourceRdbInstanceCreate(ctx context.Context, d *schema.ResourceData, m in
}
// Configure Instance settings
if settings, ok := d.GetOk("settings"); ok {
res, err = waitForRDBInstance(ctx, rdbAPI, region, res.ID, d.Timeout(schema.TimeoutCreate))
res, err := waitForRDBInstance(ctx, rdbAPI, region, id, d.Timeout(schema.TimeoutCreate))
if err != nil {
return diag.FromErr(err)
}

_, err := rdbAPI.SetInstanceSettings(&rdb.SetInstanceSettingsRequest{
_, err = rdbAPI.SetInstanceSettings(&rdb.SetInstanceSettingsRequest{
InstanceID: res.ID,
Region: region,
Settings: expandInstanceSettings(settings),
Expand Down
Loading
Loading