mysql provider and mysql_database resource.

Allows databases on pre-existing MySQL servers to be created and managed
by Terraform.
This commit is contained in:
Martin Atkins 2015-12-16 17:59:35 -08:00
parent 90eb04399a
commit a9d97708ee
11 changed files with 561 additions and 0 deletions

View File

@ -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,
})
}

View File

@ -0,0 +1 @@
package main

View File

@ -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))
}

View File

@ -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")
}
}
}

View File

@ -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 ""
}

View File

@ -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"
}
`

View File

@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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>

View File

@ -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">&laquo; 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 %>