Skip to content

Commit

Permalink
feat(rdb): creating instance from snapshot (#2872)
Browse files Browse the repository at this point in the history
* feat(rdb): add resource for snapshot

* feat: add test

* fix lint doc

* fix: lint doc

* fix: lint doc

* fix: lint doc

* fix: lint doc

* fix: lint doc

* chore(deps): bump github.com/aws/smithy-go from 1.22.1 to 1.22.2 (#2892)

Bumps [github.com/aws/smithy-go](https://github.com/aws/smithy-go) from 1.22.1 to 1.22.2.
- [Release notes](https://github.com/aws/smithy-go/releases)
- [Changelog](https://github.com/aws/smithy-go/blob/main/CHANGELOG.md)
- [Commits](aws/smithy-go@v1.22.1...v1.22.2)

---
updated-dependencies:
- dependency-name: github.com/aws/smithy-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rémy Léone <rleone@scaleway.com>

* feat: add test

* fix: name test

* fix: lint

* updated cassette

* fix error 400

* delete old cassette

* delete unused cassette

* fix delete log

* doc updated

* fix: update node_type description

* Fix a lint

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rémy Léone <rleone@scaleway.com>
  • Loading branch information
3 people authored Feb 13, 2025
1 parent b213214 commit 006df6b
Show file tree
Hide file tree
Showing 46 changed files with 25,283 additions and 17,591 deletions.
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

0 comments on commit 006df6b

Please sign in to comment.