Merge #3122: MySQL provider.
This commit is contained in:
commit
744b23bb69
|
@ -5,6 +5,7 @@ FEATURES:
|
|||
* **New provider: `postgresql` - Create PostgreSQL databases and roles** [GH-3653]
|
||||
* **New provider: `chef` - Create chef environments, roles, etc** [GH-3084]
|
||||
* **New provider: `azurerm` - Preliminary support for Azure Resource Manager** [GH-4226]
|
||||
* **New provider: `mysql` - Create MySQL databases** [GH-3122]
|
||||
* **New resource: `aws_autoscaling_schedule`** [GH-4256]
|
||||
* **New resource: `google_pubsub_topic`** [GH-3671]
|
||||
* **New resource: `google_pubsub_subscription`** [GH-3671]
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/builtin/providers/mysql"
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeOpts{
|
||||
ProviderFunc: mysql.Provider,
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -0,0 +1,71 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
mysqlc "github.com/ziutek/mymysql/thrsafe"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func Provider() terraform.ResourceProvider {
|
||||
return &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"endpoint": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("MYSQL_ENDPOINT", nil),
|
||||
},
|
||||
|
||||
"username": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("MYSQL_USERNAME", nil),
|
||||
},
|
||||
|
||||
"password": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("MYSQL_PASSWORD", nil),
|
||||
},
|
||||
},
|
||||
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"mysql_database": resourceDatabase(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
}
|
||||
}
|
||||
|
||||
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
|
||||
|
||||
var username = d.Get("username").(string)
|
||||
var password = d.Get("password").(string)
|
||||
var endpoint = d.Get("endpoint").(string)
|
||||
|
||||
proto := "tcp"
|
||||
if endpoint[0] == '/' {
|
||||
proto = "unix"
|
||||
}
|
||||
|
||||
// mysqlc is the thread-safe implementation of mymysql, so we can
|
||||
// safely re-use the same connection between multiple parallel
|
||||
// operations.
|
||||
conn := mysqlc.New(proto, "", endpoint, username, password)
|
||||
|
||||
err := conn.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
var identQuoteReplacer = strings.NewReplacer("`", "``")
|
||||
|
||||
func quoteIdentifier(in string) string {
|
||||
return fmt.Sprintf("`%s`", identQuoteReplacer.Replace(in))
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// To run these acceptance tests, you will need access to a MySQL server.
|
||||
// Amazon RDS is one way to get a MySQL server. If you use RDS, you can
|
||||
// use the root account credentials you specified when creating an RDS
|
||||
// instance to get the access necessary to run these tests. (the tests
|
||||
// assume full access to the server.)
|
||||
//
|
||||
// Set the MYSQL_ENDPOINT and MYSQL_USERNAME environment variables before
|
||||
// running the tests. If the given user has a password then you will also need
|
||||
// to set MYSQL_PASSWORD.
|
||||
//
|
||||
// The tests assume a reasonably-vanilla MySQL configuration. In particular,
|
||||
// they assume that the "utf8" character set is available and that
|
||||
// "utf8_bin" is a valid collation that isn't the default for that character
|
||||
// set.
|
||||
//
|
||||
// You can run the tests like this:
|
||||
// make testacc TEST=./builtin/providers/mysql
|
||||
|
||||
var testAccProviders map[string]terraform.ResourceProvider
|
||||
var testAccProvider *schema.Provider
|
||||
|
||||
func init() {
|
||||
testAccProvider = Provider().(*schema.Provider)
|
||||
testAccProviders = map[string]terraform.ResourceProvider{
|
||||
"mysql": testAccProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
if err := Provider().(*schema.Provider).InternalValidate(); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvider = Provider()
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
for _, name := range []string{"MYSQL_ENDPOINT", "MYSQL_USERNAME"} {
|
||||
if v := os.Getenv(name); v == "" {
|
||||
t.Fatal("MYSQL_ENDPOINT, MYSQL_USERNAME and optionally MYSQL_PASSWORD must be set for acceptance tests")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
mysqlc "github.com/ziutek/mymysql/mysql"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
const defaultCharacterSetKeyword = "CHARACTER SET "
|
||||
const defaultCollateKeyword = "COLLATE "
|
||||
|
||||
func resourceDatabase() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: CreateDatabase,
|
||||
Update: UpdateDatabase,
|
||||
Read: ReadDatabase,
|
||||
Delete: DeleteDatabase,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"default_character_set": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "utf8",
|
||||
},
|
||||
|
||||
"default_collation": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "utf8_general_ci",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDatabase(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(mysqlc.Conn)
|
||||
|
||||
stmtSQL := databaseConfigSQL("CREATE", d)
|
||||
log.Println("Executing statement:", stmtSQL)
|
||||
|
||||
_, _, err := conn.Query(stmtSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.SetId(d.Get("name").(string))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateDatabase(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(mysqlc.Conn)
|
||||
|
||||
stmtSQL := databaseConfigSQL("ALTER", d)
|
||||
log.Println("Executing statement:", stmtSQL)
|
||||
|
||||
_, _, err := conn.Query(stmtSQL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadDatabase(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(mysqlc.Conn)
|
||||
|
||||
// This is kinda flimsy-feeling, since it depends on the formatting
|
||||
// of the SHOW CREATE DATABASE output... but this data doesn't seem
|
||||
// to be available any other way, so hopefully MySQL keeps this
|
||||
// compatible in future releases.
|
||||
|
||||
name := d.Id()
|
||||
stmtSQL := "SHOW CREATE DATABASE " + quoteIdentifier(name)
|
||||
|
||||
log.Println("Executing query:", stmtSQL)
|
||||
rows, _, err := conn.Query(stmtSQL)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysqlc.Error); ok {
|
||||
if mysqlErr.Code == mysqlc.ER_BAD_DB_ERROR {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
row := rows[0]
|
||||
createSQL := string(row[1].([]byte))
|
||||
|
||||
defaultCharset := extractIdentAfter(createSQL, defaultCharacterSetKeyword)
|
||||
defaultCollation := extractIdentAfter(createSQL, defaultCollateKeyword)
|
||||
|
||||
if defaultCollation == "" && defaultCharset != "" {
|
||||
// MySQL doesn't return the collation if it's the default one for
|
||||
// the charset, so if we don't have a collation we need to go
|
||||
// hunt for the default.
|
||||
stmtSQL := "SHOW COLLATION WHERE `Charset` = '%s' AND `Default` = 'Yes'"
|
||||
rows, _, err := conn.Query(stmtSQL, defaultCharset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting default charset: %s", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return fmt.Errorf("Charset %s has no default collation", defaultCharset)
|
||||
}
|
||||
row := rows[0]
|
||||
defaultCollation = string(row[0].([]byte))
|
||||
}
|
||||
|
||||
d.Set("default_character_set", defaultCharset)
|
||||
d.Set("default_collation", defaultCollation)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteDatabase(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(mysqlc.Conn)
|
||||
|
||||
name := d.Id()
|
||||
stmtSQL := "DROP DATABASE " + quoteIdentifier(name)
|
||||
log.Println("Executing statement:", stmtSQL)
|
||||
|
||||
_, _, err := conn.Query(stmtSQL)
|
||||
if err == nil {
|
||||
d.SetId("")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func databaseConfigSQL(verb string, d *schema.ResourceData) string {
|
||||
name := d.Get("name").(string)
|
||||
defaultCharset := d.Get("default_character_set").(string)
|
||||
defaultCollation := d.Get("default_collation").(string)
|
||||
|
||||
var defaultCharsetClause string
|
||||
var defaultCollationClause string
|
||||
|
||||
if defaultCharset != "" {
|
||||
defaultCharsetClause = defaultCharacterSetKeyword + quoteIdentifier(defaultCharset)
|
||||
}
|
||||
if defaultCollation != "" {
|
||||
defaultCollationClause = defaultCollateKeyword + quoteIdentifier(defaultCollation)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s DATABASE %s %s %s",
|
||||
verb,
|
||||
quoteIdentifier(name),
|
||||
defaultCharsetClause,
|
||||
defaultCollationClause,
|
||||
)
|
||||
}
|
||||
|
||||
func extractIdentAfter(sql string, keyword string) string {
|
||||
charsetIndex := strings.Index(sql, keyword)
|
||||
if charsetIndex != -1 {
|
||||
charsetIndex += len(keyword)
|
||||
remain := sql[charsetIndex:]
|
||||
spaceIndex := strings.IndexRune(remain, ' ')
|
||||
return remain[:spaceIndex]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mysqlc "github.com/ziutek/mymysql/mysql"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestAccDatabase(t *testing.T) {
|
||||
var dbName string
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccDatabaseCheckDestroy(dbName),
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccDatabaseConfig_basic,
|
||||
Check: testAccDatabaseCheck(
|
||||
"mysql_database.test", &dbName,
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccDatabaseCheck(rn string, name *string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[rn]
|
||||
if !ok {
|
||||
return fmt.Errorf("resource not found: %s", rn)
|
||||
}
|
||||
|
||||
if rs.Primary.ID == "" {
|
||||
return fmt.Errorf("database id not set")
|
||||
}
|
||||
|
||||
conn := testAccProvider.Meta().(mysqlc.Conn)
|
||||
rows, _, err := conn.Query("SHOW CREATE DATABASE terraform_acceptance_test")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading database: %s", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
return fmt.Errorf("expected 1 row reading database but got %d", len(rows))
|
||||
}
|
||||
|
||||
row := rows[0]
|
||||
createSQL := string(row[1].([]byte))
|
||||
|
||||
if strings.Index(createSQL, "CHARACTER SET utf8") == -1 {
|
||||
return fmt.Errorf("database default charset isn't utf8")
|
||||
}
|
||||
if strings.Index(createSQL, "COLLATE utf8_bin") == -1 {
|
||||
return fmt.Errorf("database default collation isn't utf8_bin")
|
||||
}
|
||||
|
||||
*name = rs.Primary.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccDatabaseCheckDestroy(name string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
conn := testAccProvider.Meta().(mysqlc.Conn)
|
||||
|
||||
_, _, err := conn.Query("SHOW CREATE DATABASE terraform_acceptance_test")
|
||||
if err == nil {
|
||||
return fmt.Errorf("database still exists after destroy")
|
||||
}
|
||||
if mysqlErr, ok := err.(*mysqlc.Error); ok {
|
||||
if mysqlErr.Code == mysqlc.ER_BAD_DB_ERROR {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("got unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
const testAccDatabaseConfig_basic = `
|
||||
resource "mysql_database" "test" {
|
||||
name = "terraform_acceptance_test"
|
||||
default_character_set = "utf8"
|
||||
default_collation = "utf8_bin"
|
||||
}
|
||||
`
|
|
@ -22,6 +22,7 @@ body.layout-dyn,
|
|||
body.layout-google,
|
||||
body.layout-heroku,
|
||||
body.layout-mailgun,
|
||||
body.layout-mysql,
|
||||
body.layout-openstack,
|
||||
body.layout-packet,
|
||||
body.layout-postgresql,
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
layout: "mysql"
|
||||
page_title: "Provider: MySQL"
|
||||
sidebar_current: "docs-mysql-index"
|
||||
description: |-
|
||||
A provider for MySQL Server.
|
||||
---
|
||||
|
||||
# MySQL Provider
|
||||
|
||||
[MySQL](http://www.mysql.com) is a relational database server. The MySQL
|
||||
provider exposes resources used to manage the configuration of resources
|
||||
in a MySQL server.
|
||||
|
||||
Use the navigation to the left to read about the available resources.
|
||||
|
||||
## Example Usage
|
||||
|
||||
The following is a minimal example:
|
||||
|
||||
```
|
||||
# Configure the MySQL provider
|
||||
provider "mysql" {
|
||||
endpoint = "my-database.example.com:3306"
|
||||
username = "app-user"
|
||||
password = "app-password"
|
||||
}
|
||||
|
||||
# Create a Database
|
||||
resource "mysql_database" "app" {
|
||||
name = "my_awesome_app"
|
||||
}
|
||||
```
|
||||
|
||||
This provider can be used in conjunction with other resources that create
|
||||
MySQL servers. For example, ``aws_db_instance`` is able to create MySQL
|
||||
servers in Amazon's RDS service.
|
||||
|
||||
```
|
||||
# Create a database server
|
||||
resource "aws_db_instance" "default" {
|
||||
engine = "mysql"
|
||||
engine_version = "5.6.17"
|
||||
instance_class = "db.t1.micro"
|
||||
name = "initial_db"
|
||||
username = "rootuser"
|
||||
password = "rootpasswd"
|
||||
# etc, etc; see aws_db_instance docs for more
|
||||
}
|
||||
|
||||
# Configure the MySQL provider based on the outcome of
|
||||
# creating the aws_db_instance.
|
||||
provider "mysql" {
|
||||
endpoint = "${aws_db_instance.default.endpoint}"
|
||||
username = "${aws_db_instance.default.username}"
|
||||
password = "${aws_db_instance.default.password}"
|
||||
}
|
||||
|
||||
# Create a second database, in addition to the "initial_db" created
|
||||
# by the aws_db_instance resource above.
|
||||
resource "mysql_database" "app" {
|
||||
name = "another_db"
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `endpoint` - (Required) The address of the MySQL server to use. Most often a "hostname:port" pair, but may also be an absolute path to a Unix socket when the host OS is Unix-compatible.
|
||||
* `username` - (Required) Username to use to authenticate with the server.
|
||||
* `password` - (Optional) Password for the given user, if that user has a password.
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
layout: "mysql"
|
||||
page_title: "MySQL: mysql_database"
|
||||
sidebar_current: "docs-mysql-resource-database"
|
||||
description: |-
|
||||
Creates and manages a database on a MySQL server.
|
||||
---
|
||||
|
||||
# mysql\_database
|
||||
|
||||
The ``mysql_database`` resource creates and manages a database on a MySQL
|
||||
server.
|
||||
|
||||
~> **Caution:** The ``mysql_database`` resource can completely delete your
|
||||
database just as easily as it can create it. To avoid costly accidents,
|
||||
consider setting
|
||||
[``prevent_destroy``](/docs/configuration/resources.html#prevent_destroy)
|
||||
on your database resources as an extra safety measure.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
resource "mysql_database" "app" {
|
||||
name = "my_awesome_app"
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `name` - (Required) The name of the database. This must be unique within
|
||||
a given MySQL server and may or may not be case-sensitive depending on
|
||||
the operating system on which the MySQL server is running.
|
||||
|
||||
* `default_character_set` - (Optional) The default character set to use when
|
||||
a table is created without specifying an explicit character set. Defaults
|
||||
to "utf8".
|
||||
|
||||
* `default_collation` - (Optional) The default collation to use when a table
|
||||
is created without specifying an explicit collation. Defaults to
|
||||
``utf8_general_ci``. Each character set has its own set of collations, so
|
||||
changing the character set requires also changing the collation.
|
||||
|
||||
Note that the defaults for character set and collation above do not respect
|
||||
any defaults set on the MySQL server, so that the configuration can be set
|
||||
appropriately even though Terraform cannot see the server-level defaults. If
|
||||
you wish to use the server's defaults you must consult the server's
|
||||
configuration and then set the ``default_character_set`` and
|
||||
``default_collation`` to match.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
No further attributes are exported.
|
|
@ -185,6 +185,10 @@
|
|||
<a href="/docs/providers/mailgun/index.html">Mailgun</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-mysql") %>>
|
||||
<a href="/docs/providers/mysql/index.html">MySQL</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-providers-openstack") %>>
|
||||
<a href="/docs/providers/openstack/index.html">OpenStack</a>
|
||||
</li>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<% wrap_layout :inner do %>
|
||||
<% content_for :sidebar do %>
|
||||
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||
<ul class="nav docs-sidenav">
|
||||
<li<%= sidebar_current("docs-home") %>>
|
||||
<a href="/docs/providers/index.html">« Documentation Home</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-mysql-index") %>>
|
||||
<a href="/docs/providers/mysql/index.html">MySQL Provider</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current(/^docs-mysql-resource/) %>>
|
||||
<a href="#">Resources</a>
|
||||
<ul class="nav nav-visible">
|
||||
<li<%= sidebar_current("docs-mysql-resource-database") %>>
|
||||
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
<% end %>
|
Loading…
Reference in New Issue