diff --git a/digitalocean/import_digitalocean_database_connection_pool_test.go b/digitalocean/import_digitalocean_database_connection_pool_test.go new file mode 100644 index 00000000..c10b71a6 --- /dev/null +++ b/digitalocean/import_digitalocean_database_connection_pool_test.go @@ -0,0 +1,58 @@ +package digitalocean + +import ( + "testing" + + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccDigitalOceanDatabaseConnectionPool_importBasic(t *testing.T) { + resourceName := "digitalocean_database_connection_pool.pool-01" + databaseName := randomTestName() + databaseConnectionPoolName := randomTestName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDatabaseConnectionPoolDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseConnectionPoolConfigBasic, databaseName, databaseConnectionPoolName), + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // Requires passing both the cluster ID and pool name + ImportStateIdFunc: testAccDatabasePoolImportID(resourceName), + }, + // Test importing non-existent resource provides expected error. + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: false, + ImportStateId: fmt.Sprintf("%s,%s", "this-cluster-id-does-not-exist", databaseConnectionPoolName), + ExpectError: regexp.MustCompile(`(Please verify the ID is correct|Cannot import non-existent remote object)`), + }, + }, + }) +} + +func testAccDatabasePoolImportID(n string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[n] + if !ok { + return "", fmt.Errorf("Not found: %s", n) + } + + clusterId := rs.Primary.Attributes["cluster_id"] + name := rs.Primary.Attributes["name"] + + return fmt.Sprintf("%s,%s", clusterId, name), nil + } +} diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 782dec6c..0f38b438 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -58,29 +58,30 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "digitalocean_certificate": resourceDigitalOceanCertificate(), - "digitalocean_cdn": resourceDigitalOceanCDN(), - "digitalocean_database_cluster": resourceDigitalOceanDatabaseCluster(), - "digitalocean_database_db": resourceDigitalOceanDatabaseDB(), - "digitalocean_database_replica": resourceDigitalOceanDatabaseReplica(), - "digitalocean_database_user": resourceDigitalOceanDatabaseUser(), - "digitalocean_domain": resourceDigitalOceanDomain(), - "digitalocean_droplet": resourceDigitalOceanDroplet(), - "digitalocean_droplet_snapshot": resourceDigitalOceanDropletSnapshot(), - "digitalocean_firewall": resourceDigitalOceanFirewall(), - "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), - "digitalocean_floating_ip_assignment": resourceDigitalOceanFloatingIpAssignment(), - "digitalocean_kubernetes_cluster": resourceDigitalOceanKubernetesCluster(), - "digitalocean_kubernetes_node_pool": resourceDigitalOceanKubernetesNodePool(), - "digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(), - "digitalocean_project": resourceDigitalOceanProject(), - "digitalocean_record": resourceDigitalOceanRecord(), - "digitalocean_spaces_bucket": resourceDigitalOceanBucket(), - "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), - "digitalocean_tag": resourceDigitalOceanTag(), - "digitalocean_volume": resourceDigitalOceanVolume(), - "digitalocean_volume_attachment": resourceDigitalOceanVolumeAttachment(), - "digitalocean_volume_snapshot": resourceDigitalOceanVolumeSnapshot(), + "digitalocean_certificate": resourceDigitalOceanCertificate(), + "digitalocean_cdn": resourceDigitalOceanCDN(), + "digitalocean_database_cluster": resourceDigitalOceanDatabaseCluster(), + "digitalocean_database_connection_pool": resourceDigitalOceanDatabaseConnectionPool(), + "digitalocean_database_db": resourceDigitalOceanDatabaseDB(), + "digitalocean_database_replica": resourceDigitalOceanDatabaseReplica(), + "digitalocean_database_user": resourceDigitalOceanDatabaseUser(), + "digitalocean_domain": resourceDigitalOceanDomain(), + "digitalocean_droplet": resourceDigitalOceanDroplet(), + "digitalocean_droplet_snapshot": resourceDigitalOceanDropletSnapshot(), + "digitalocean_firewall": resourceDigitalOceanFirewall(), + "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), + "digitalocean_floating_ip_assignment": resourceDigitalOceanFloatingIpAssignment(), + "digitalocean_kubernetes_cluster": resourceDigitalOceanKubernetesCluster(), + "digitalocean_kubernetes_node_pool": resourceDigitalOceanKubernetesNodePool(), + "digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(), + "digitalocean_project": resourceDigitalOceanProject(), + "digitalocean_record": resourceDigitalOceanRecord(), + "digitalocean_spaces_bucket": resourceDigitalOceanBucket(), + "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), + "digitalocean_tag": resourceDigitalOceanTag(), + "digitalocean_volume": resourceDigitalOceanVolume(), + "digitalocean_volume_attachment": resourceDigitalOceanVolumeAttachment(), + "digitalocean_volume_snapshot": resourceDigitalOceanVolumeSnapshot(), }, } diff --git a/digitalocean/resource_digitalocean_database_connection_pool.go b/digitalocean/resource_digitalocean_database_connection_pool.go new file mode 100644 index 00000000..36216d29 --- /dev/null +++ b/digitalocean/resource_digitalocean_database_connection_pool.go @@ -0,0 +1,199 @@ +package digitalocean + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceDigitalOceanDatabaseConnectionPool() *schema.Resource { + return &schema.Resource{ + Create: resourceDigitalOceanDatabaseConnectionPoolCreate, + Read: resourceDigitalOceanDatabaseConnectionPoolRead, + Delete: resourceDigitalOceanDatabaseConnectionPoolDelete, + Importer: &schema.ResourceImporter{ + State: resourceDigitalOceanDatabaseConnectionPoolImport, + }, + + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "user": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "mode": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "session", + "transaction", + "statement"}, false), + }, + + "size": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "db_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "host": { + Type: schema.TypeString, + Computed: true, + }, + + "private_host": { + Type: schema.TypeString, + Computed: true, + }, + + "port": { + Type: schema.TypeInt, + Computed: true, + }, + + "uri": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "private_uri": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + } +} + +func resourceDigitalOceanDatabaseConnectionPoolCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*CombinedConfig).godoClient() + + clusterID := d.Get("cluster_id").(string) + opts := &godo.DatabaseCreatePoolRequest{ + Name: d.Get("name").(string), + User: d.Get("user").(string), + Mode: d.Get("mode").(string), + Size: d.Get("size").(int), + Database: d.Get("db_name").(string), + } + + log.Printf("[DEBUG] DatabaseConnectionPool create configuration: %#v", opts) + pool, _, err := client.Databases.CreatePool(context.Background(), clusterID, opts) + if err != nil { + return fmt.Errorf("Error creating DatabaseConnectionPool: %s", err) + } + + d.SetId(createConnectionPoolID(clusterID, pool.Name)) + log.Printf("[INFO] DatabaseConnectionPool Name: %s", pool.Name) + + return resourceDigitalOceanDatabaseConnectionPoolRead(d, meta) +} + +func resourceDigitalOceanDatabaseConnectionPoolRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*CombinedConfig).godoClient() + clusterID, poolName := splitConnectionPoolID(d.Id()) + + pool, resp, err := client.Databases.GetPool(context.Background(), clusterID, poolName) + if err != nil { + // If the pool is somehow already destroyed, mark as + // successfully gone + if resp.StatusCode == 404 { + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving DatabaseConnectionPool: %s", err) + } + + d.SetId(createConnectionPoolID(clusterID, pool.Name)) + d.Set("cluster_id", clusterID) + d.Set("name", pool.Name) + d.Set("user", pool.User) + d.Set("mode", pool.Mode) + d.Set("size", pool.Size) + d.Set("db_name", pool.Database) + + // Computed values + d.Set("host", pool.Connection.Host) + d.Set("private_host", pool.PrivateConnection.Host) + d.Set("port", pool.Connection.Port) + d.Set("uri", pool.Connection.URI) + d.Set("private_uri", pool.PrivateConnection.URI) + d.Set("password", pool.Connection.Password) + + return nil +} + +func resourceDigitalOceanDatabaseConnectionPoolImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if strings.Contains(d.Id(), ",") { + s := strings.Split(d.Id(), ",") + d.SetId(createConnectionPoolID(s[0], s[1])) + d.Set("cluster_id", s[0]) + d.Set("name", s[1]) + } + + return []*schema.ResourceData{d}, nil +} + +func resourceDigitalOceanDatabaseConnectionPoolDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*CombinedConfig).godoClient() + clusterID, poolName := splitConnectionPoolID(d.Id()) + + log.Printf("[INFO] Deleting DatabaseConnectionPool: %s", poolName) + _, err := client.Databases.DeletePool(context.Background(), clusterID, poolName) + if err != nil { + return fmt.Errorf("Error deleting DatabaseConnectionPool: %s", err) + } + + d.SetId("") + return nil +} + +func createConnectionPoolID(clusterID string, poolName string) string { + return fmt.Sprintf("%s/%s", clusterID, poolName) +} + +func splitConnectionPoolID(id string) (string, string) { + splitID := strings.Split(id, "/") + clusterID := splitID[0] + poolName := splitID[1] + + return clusterID, poolName +} diff --git a/digitalocean/resource_digitalocean_database_connection_pool_test.go b/digitalocean/resource_digitalocean_database_connection_pool_test.go new file mode 100644 index 00000000..f9f33a08 --- /dev/null +++ b/digitalocean/resource_digitalocean_database_connection_pool_test.go @@ -0,0 +1,203 @@ +package digitalocean + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccDigitalOceanDatabaseConnectionPool_Basic(t *testing.T) { + var databaseConnectionPool godo.DatabasePool + databaseName := randomTestName() + databaseConnectionPoolName := randomTestName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDatabaseConnectionPoolDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseConnectionPoolConfigBasic, databaseName, databaseConnectionPoolName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDatabaseConnectionPoolExists("digitalocean_database_connection_pool.pool-01", &databaseConnectionPool), + testAccCheckDigitalOceanDatabaseConnectionPoolAttributes(&databaseConnectionPool, databaseConnectionPoolName), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "name", databaseConnectionPoolName), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "size", "10"), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "mode", "transaction"), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "db_name", "defaultdb"), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "user", "doadmin"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "host"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "private_host"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "port"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "uri"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "private_uri"), + resource.TestCheckResourceAttrSet( + "digitalocean_database_connection_pool.pool-01", "password"), + ), + }, + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseConnectionPoolConfigUpdated, databaseName, databaseConnectionPoolName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDatabaseConnectionPoolExists("digitalocean_database_connection_pool.pool-01", &databaseConnectionPool), + testAccCheckDigitalOceanDatabaseConnectionPoolAttributes(&databaseConnectionPool, databaseConnectionPoolName), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "name", databaseConnectionPoolName), + resource.TestCheckResourceAttr( + "digitalocean_database_connection_pool.pool-01", "mode", "session"), + ), + }, + }, + }) +} + +func TestAccDigitalOceanDatabaseConnectionPool_BadModeName(t *testing.T) { + databaseName := randomTestName() + databaseConnectionPoolName := randomTestName() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDatabaseConnectionPoolDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseConnectionPoolConfigBad, databaseName, databaseConnectionPoolName), + ExpectError: regexp.MustCompile(`expected mode to be one of`), + }, + }, + }) +} + +func testAccCheckDigitalOceanDatabaseConnectionPoolDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*CombinedConfig).godoClient() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "digitalocean_database_connection_pool" { + continue + } + clusterId := rs.Primary.Attributes["cluster_id"] + name := rs.Primary.Attributes["name"] + // Try to find the database connection_pool + _, _, err := client.Databases.GetPool(context.Background(), clusterId, name) + + if err == nil { + return fmt.Errorf("DatabaseConnectionPool still exists") + } + } + + return nil +} + +func testAccCheckDigitalOceanDatabaseConnectionPoolExists(n string, database *godo.DatabasePool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No DatabaseConnectionPool ID is set") + } + + client := testAccProvider.Meta().(*CombinedConfig).godoClient() + clusterId := rs.Primary.Attributes["cluster_id"] + name := rs.Primary.Attributes["name"] + + foundDatabaseConnectionPool, _, err := client.Databases.GetPool(context.Background(), clusterId, name) + + if err != nil { + return err + } + + if foundDatabaseConnectionPool.Name != name { + return fmt.Errorf("DatabaseConnectionPool not found") + } + + *database = *foundDatabaseConnectionPool + + return nil + } +} + +func testAccCheckDigitalOceanDatabaseConnectionPoolAttributes(databaseConnectionPool *godo.DatabasePool, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if databaseConnectionPool.Name != name { + return fmt.Errorf("Bad name: %s", databaseConnectionPool.Name) + } + + return nil + } +} + +const testAccCheckDigitalOceanDatabaseConnectionPoolConfigBasic = ` +resource "digitalocean_database_cluster" "foobar" { + name = "%s" + engine = "pg" + version = "11" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 +} + +resource "digitalocean_database_connection_pool" "pool-01" { + cluster_id = digitalocean_database_cluster.foobar.id + name = "%s" + mode = "transaction" + size = 10 + db_name = "defaultdb" + user = "doadmin" +}` + +const testAccCheckDigitalOceanDatabaseConnectionPoolConfigUpdated = ` +resource "digitalocean_database_cluster" "foobar" { + name = "%s" + engine = "pg" + version = "11" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 +} + +resource "digitalocean_database_connection_pool" "pool-01" { + cluster_id = digitalocean_database_cluster.foobar.id + name = "%s" + mode = "session" + size = 10 + db_name = "defaultdb" + user = "doadmin" +}` + +const testAccCheckDigitalOceanDatabaseConnectionPoolConfigBad = ` +resource "digitalocean_database_cluster" "foobar" { + name = "%s" + engine = "pg" + version = "11" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 +} + +resource "digitalocean_database_connection_pool" "pool-01" { + cluster_id = digitalocean_database_cluster.foobar.id + name = "%s" + mode = "transactional" + size = 10 + db_name = "defaultdb" + user = "doadmin" +}` diff --git a/website/digitalocean.erb b/website/digitalocean.erb index 38c596ac..d30e0482 100644 --- a/website/digitalocean.erb +++ b/website/digitalocean.erb @@ -76,6 +76,9 @@ > digitalocean_database_cluster + > + digitalocean_database_connection_pool + > digitalocean_database_db diff --git a/website/docs/r/database_connection_pool.html.markdown b/website/docs/r/database_connection_pool.html.markdown new file mode 100644 index 00000000..65c010a3 --- /dev/null +++ b/website/docs/r/database_connection_pool.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_database_connection_pool" +sidebar_current: "docs-do-resource-database-connection_pool" +description: |- + Provides a DigitalOcean database connection pool resource. +--- + +# digitalocean\_database\_connection\_pool + +Provides a DigitalOcean database connection pool resource. + +## Example Usage + +### Create a new PostgreSQL database connection pool +```hcl +resource "digitalocean_database_connection_pool" "pool-01" { + cluster_id = digitalocean_database_cluster.postgres-example.id + name = "pool-01" + mode = "transaction" + size = 20 + db_name = "defaultdb" + user = "doadmin" +} + +resource "digitalocean_database_cluster" "postgres-example" { + name = "example-postgres-cluster" + engine = "pg" + version = "11" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_id` - (Required) The ID of the source database cluster. Note: This must be a PostgreSQL cluster. +* `name` - (Required) The name for the database connection pool. +* `mode` - (Required) The PGBouncer transaction mode for the connection pool. The allowed values are session, transaction, and statement. +* `size` - (Required) The desired size of the PGBouncer connection pool. +* `db_name` - (Required) The database for use with the connection pool. +* `user` - (Required) The name of the database user for use with the connection pool. + +## Attributes Reference + +In addition to the above arguments, the following attributes are exported: + +* `id` - The ID of the database connection pool. +* `host` - The hostname used to connect to the database connection pool. +* `private_host` - Same as `host`, but only accessible from resources within the account and in the same region. +* `port` - Network port that the database connection pool is listening on. +* `uri` - The full URI for connecting to the database connection pool. +* `private_uri` - Same as `uri`, but only accessible from resources within the account and in the same region. +* `password` - Password for the connection pool's user. + +## Import + +Database connection pools can be imported using the `id` of the source database cluster +and the `name` of the connection pool joined with a comma. For example: + +``` +terraform import digitalocean_database_connection_pool.pool-01 245bcfd0-7f31-4ce6-a2bc-475a116cca97,pool-01 +``` diff --git a/website/docs/r/database_replica.html.markdown b/website/docs/r/database_replica.html.markdown index bf23fa12..984ac00e 100644 --- a/website/docs/r/database_replica.html.markdown +++ b/website/docs/r/database_replica.html.markdown @@ -15,7 +15,7 @@ Provides a DigitalOcean database replica resource. ### Create a new PostgreSQL database replica ```hcl resource "digitalocean_database_replica" "read-replica" { - cluster_id = "${digitalocean_database_cluster.postgres-example.id}" + cluster_id = digitalocean_database_cluster.postgres-example.id name = "read-replica" size = "db-s-1vcpu-1gb" region = "nyc1"