Skip to content

Commit

Permalink
Add linode_database_postgresql_v2 resource (#1680)
Browse files Browse the repository at this point in the history
* Implement linode_database_postgresql_v2 resource

* Add unit package

* Use events for update polling logic

* Update databaseaccesscontrols test case

* Bump replacement

* Remove RUN_LONG_TEST warning

* Use event-based polling in linode_database_access_controls resource

* Don't rely on state for update comparisons

* Remove allow_list default from schema

* Update tests for PostgreSQL default allow_list

* Drop allow_list default

* Remove replacement from go.mod

* Re-bump linodego

* Bump minimum Go version

* bump minimum go version in tools package

* Unpin patch version
  • Loading branch information
lgarber-akamai authored Jan 13, 2025
1 parent 5deb2e0 commit 7aeb26e
Show file tree
Hide file tree
Showing 23 changed files with 1,985 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
echo "LINODE_TOKEN=${{ secrets.LINODE_TOKEN_USER_2 }}" >> $GITHUB_ENV
;;
"USER_3")
echo "TEST_TAGS=instanceconfig,instancedisk,instanceip,networkingip,objcluster,objkey,profile,rdns,region,regions,stackscript,stackscripts" >> $GITHUB_ENV
echo "TEST_TAGS=databasepostgresqlv2,instanceconfig,instancedisk,instanceip,networkingip,objcluster,objkey,profile,rdns,region,regions,stackscript,stackscripts" >> $GITHUB_ENV
echo "LINODE_TOKEN=${{ secrets.LINODE_TOKEN_USER_3 }}" >> $GITHUB_ENV
;;
"USER_4")
Expand Down
164 changes: 164 additions & 0 deletions docs/resources/database_postgresql_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
page_title: "Linode: linode_database_postgresql_v2"
description: |-
Manages a Linode PostgreSQL Database.
---

# linode\_database\_postgresql\_v2

Provides a Linode PostgreSQL Database resource. This can be used to create, modify, and delete Linode PostgreSQL Databases.
For more information, see the [Linode APIv4 docs](https://techdocs.akamai.com/linode-api/reference/post-databases-postgre-sql-instances).

Please keep in mind that Managed Databases can take up to half an hour to provision.

## Example Usage

Creating a simple PostgreSQL database that does not allow connections:

```hcl
resource "linode_database_postgresql_v2" "foobar" {
label = "mydatabase"
engine_id = "postgresql/16"
region = "us-mia"
type = "g6-nanode-1"
}
```

Creating a simple PostgreSQL database that allows connections from all IPv4 addresses:

```hcl
resource "linode_database_postgresql_v2" "foobar" {
label = "mydatabase"
engine_id = "postgresql/16"
region = "us-mia"
type = "g6-nanode-1"
allowed_ips = ["0.0.0.0/0"]
}
```

Creating a complex PostgreSQL database:

```hcl
resource "linode_database_postgresql_v2" "foobar" {
label = "mydatabase"
engine_id = "postgresql/16"
region = "us-mia"
type = "g6-nanode-1"
allow_list = ["10.0.0.3/32"]
cluster_size = 3
updates {
duration = 4
frequency = "weekly"
hour_of_day = 22
week_of_month = 2
}
}
```

Creating a forked PostgreSQL database:

```hcl
resource "linode_database_postgresql_v2" "foobar" {
label = "mydatabase"
engine_id = "postgresql/16"
region = "us-mia"
type = "g6-nanode-1"
fork_source = 12345
}
```

## Argument Reference

The following arguments are supported:

* `engine_id` - (Required) The Managed Database engine in engine/version format. (e.g. `postgresql/16`)

* `label` - (Required) A unique, user-defined string referring to the Managed Database.

* `region` - (Required) The region to use for the Managed Database.

* `type` - (Required) The Linode Instance type used for the nodes of the Managed Database.

- - -

* `allow_list` - (Optional) A list of IP addresses that can access the Managed Database. Each item can be a single IP address or a range in CIDR format. Use `linode_database_access_controls` to manage your allow list separately.

* `cluster_size` - (Optional) The number of Linode Instance nodes deployed to the Managed Database. (default `1`)

* `fork_restore_time` - (Optional) The database timestamp from which it was restored.

* `fork_source` - (Optional) The ID of the database that was forked from.

* [`updates`](#updates) - (Optional) Configuration settings for automated patch update maintenance for the Managed Database.

## Attributes Reference

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

* `id` - The ID of the Managed Database.

* `ca_cert` - The base64-encoded SSL CA certificate for the Managed Database.

* `created` - When this Managed Database was created.

* `encrypted` - Whether the Managed Databases is encrypted.

* `engine` - The Managed Database engine. (e.g. `postgresql`)

* `host_primary` - The primary host for the Managed Database.

* `host_secondary` - The secondary/private host for the managed database.

* `pending_updates` - A set of pending updates.

* `platform` - The back-end platform for relational databases used by the service.

* `port` - The access port for this Managed Database.

* `root_password` - The randomly-generated root password for the Managed Database instance.

* `root_username` - The root username for the Managed Database instance.

* `ssl_connection` - Whether to require SSL credentials to establish a connection to the Managed Database.

* `status` - The operating status of the Managed Database.

* `updated` - When this Managed Database was last updated.

* `version` - The Managed Database engine version. (e.g. `13.2`)

## pending_updates

The following arguments are exposed by each entry in the `pending_updates` attribute:

* `deadline` - The time when a mandatory update needs to be applied.

* `description` - A description of the update.

* `planned_for` - The date and time a maintenance update will be applied.

## updates

The following arguments are supported in the `updates` specification block:

* `day_of_week` - (Required) The day to perform maintenance. (`monday`, `tuesday`, ...)

* `duration` - (Required) The maximum maintenance window time in hours. (`1`..`3`)

* `frequency` - (Required) Whether maintenance occurs on a weekly or monthly basis. (`weekly`, `monthly`)

* `hour_of_day` - (Required) The hour to begin maintenance based in UTC time. (`0`..`23`)

* `week_of_month` - (Optional) The week of the month to perform monthly frequency updates. Required for `monthly` frequency updates. (`1`..`4`)

## Import

Linode PostgreSQL Databases can be imported using the `id`, e.g.

```sh
terraform import linode_database_postgresql_v2.foobar 1234567
```
8 changes: 3 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/linode/terraform-provider-linode/v2

go 1.22.0

toolchain go1.22.5
go 1.23

require (
github.com/aws/aws-sdk-go-v2 v1.32.8
Expand All @@ -26,7 +24,7 @@ require (
github.com/hashicorp/terraform-plugin-mux v0.17.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0
github.com/hashicorp/terraform-plugin-testing v1.11.0
github.com/linode/linodego v1.44.1
github.com/linode/linodego v1.45.0
github.com/linode/linodego/k8s v1.25.2
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.32.0
Expand Down Expand Up @@ -102,7 +100,7 @@ require (
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/linode/linodego v1.44.1 h1:+O1KUjJLe4Y6hVXFgN4+VZh+06JaPTsZHonup/pHPN0=
github.com/linode/linodego v1.44.1/go.mod h1:gbgZweiU1LFyaCKI12wUlwDTeha/JTGruoKj751Ix5Q=
github.com/linode/linodego v1.45.0 h1:jds704+yDlxX+1s854Bhyeonzde3MBZksp3Yvgi49L0=
github.com/linode/linodego v1.45.0/go.mod h1:J5qs5Qg8KafUbE9ltYzwxcNpJXaGwLcvOVyDJNZS2As=
github.com/linode/linodego/k8s v1.25.2 h1:PY6S0sAD3xANVvM9WY38bz9GqMTjIbytC8IJJ9Cv23o=
github.com/linode/linodego/k8s v1.25.2/go.mod h1:DC1XCSRZRGsmaa/ggpDPSDUmOM6aK1bhSIP6+f9Cwhc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
Expand Down Expand Up @@ -287,8 +287,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
83 changes: 59 additions & 24 deletions linode/databaseaccesscontrols/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"log"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -71,9 +70,14 @@ func updateResource(ctx context.Context, d *schema.ResourceData, meta interface{
}

if d.HasChange("allow_list") {
allowList := helper.ExpandStringSet(d.Get("allow_list").(*schema.Set))

if err := updateDBAllowListByEngine(ctx, client, d, dbType, dbID, allowList); err != nil {
if err := updateDBAllowListByEngine(
ctx,
client,
d,
dbType,
dbID,
d.Get("allow_list").(*schema.Set),
); err != nil {
return diag.Errorf("failed to update allow_list for database %d: %s", dbID, err)
}
}
Expand All @@ -89,7 +93,14 @@ func deleteResource(ctx context.Context, d *schema.ResourceData, meta interface{
return diag.Errorf("failed to parse database id: %s", err)
}

if err := updateDBAllowListByEngine(ctx, client, d, dbType, dbID, []string{}); err != nil {
if err := updateDBAllowListByEngine(
ctx,
client,
d,
dbType,
dbID,
schema.NewSet(schema.HashString, []any{}),
); err != nil {
return diag.Errorf("failed to update allow_list for database %d: %s", dbID, err)
}

Expand All @@ -98,59 +109,83 @@ func deleteResource(ctx context.Context, d *schema.ResourceData, meta interface{
return nil
}

func updateDBAllowListByEngine(ctx context.Context, client linodego.Client, d *schema.ResourceData,
engine string, id int, allowList []string,
func updateDBAllowListByEngine(
ctx context.Context,
client linodego.Client,
d *schema.ResourceData,
engine string,
id int,
allowList *schema.Set,
) error {
var createdDate *time.Time
// Rather than using the state, we retrieve and compare DB allow lists here
// to account for a case where the state is not populated during creation.
//
// This is necessary because database_update events are only created when the value
// of allow_list is actually changed.
oldAllowList, err := getDBAllowListByEngine(ctx, client, engine, id)
if err != nil {
return fmt.Errorf("failed to get allow_list for database: %w", err)
}

if oldAllowList.Equal(allowList) {
// Nothing to do here
return nil
}

updatePoller, err := client.NewEventPoller(ctx, id, linodego.EntityDatabase, linodego.ActionDatabaseUpdate)
if err != nil {
return fmt.Errorf("failed to create update EventPoller: %w", err)
}

allowListSlice := helper.AnySliceToTyped[string](allowList.List())

switch engine {
case "mysql":
db, err := client.UpdateMySQLDatabase(ctx, id, linodego.MySQLUpdateOptions{
AllowList: &allowList,
})
if err != nil {
if _, err := client.UpdateMySQLDatabase(ctx, id, linodego.MySQLUpdateOptions{
AllowList: &allowListSlice,
}); err != nil {
return err
}

createdDate = db.Created
case "postgresql":
db, err := client.UpdatePostgresDatabase(ctx, id, linodego.PostgresUpdateOptions{
AllowList: &allowList,
})
if err != nil {
if _, err := client.UpdatePostgresDatabase(ctx, id, linodego.PostgresUpdateOptions{
AllowList: &allowListSlice,
}); err != nil {
return err
}

createdDate = db.Created

default:
return fmt.Errorf("invalid database engine: %s", engine)
}

timeoutSeconds, err := helper.SafeFloat64ToInt(d.Timeout(schema.TimeoutUpdate).Seconds())
if err != nil {
return err
}

return helper.WaitForDatabaseUpdated(ctx, client, id, linodego.DatabaseEngineType(engine),
createdDate, timeoutSeconds)
if _, err := updatePoller.WaitForFinished(ctx, timeoutSeconds); err != nil {
return fmt.Errorf("failed to wait for update event completion: %w", err)
}

return nil
}

func getDBAllowListByEngine(ctx context.Context, client linodego.Client, engine string, id int) ([]string, error) {
func getDBAllowListByEngine(ctx context.Context, client linodego.Client, engine string, id int) (*schema.Set, error) {
switch engine {
case "mysql":
db, err := client.GetMySQLDatabase(ctx, id)
if err != nil {
return nil, err
}

return db.AllowList, nil
return schema.NewSet(schema.HashString, helper.FlattenToInterfaceSlice(db.AllowList)), nil
case "postgresql":
db, err := client.GetPostgresDatabase(ctx, id)
if err != nil {
return nil, err
}

return db.AllowList, nil
return schema.NewSet(schema.HashString, helper.FlattenToInterfaceSlice(db.AllowList)), nil
}

return nil, fmt.Errorf("invalid database type: %s", engine)
Expand Down
1 change: 0 additions & 1 deletion linode/databaseaccesscontrols/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ func TestAccResourceDatabaseAccessControls_MySQL(t *testing.T) {
}

func TestAccResourceDatabaseAccessControls_PostgreSQL(t *testing.T) {
acceptance.LongRunningTest(t)
t.Parallel()

resName := "linode_database_access_controls.foobar"
Expand Down
1 change: 1 addition & 0 deletions linode/databaseaccesscontrols/schema_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var resourceSchema = map[string]*schema.Schema{
Description: "A list of IP addresses that can access the Managed Database. " +
"Each item can be a single IP address or a range in CIDR format.",
Required: true,
Set: schema.HashString,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
Loading

0 comments on commit 7aeb26e

Please sign in to comment.