Skip to content

Commit

Permalink
Merge #3122: MySQL provider.
Browse files Browse the repository at this point in the history
  • Loading branch information
apparentlymart committed Dec 17, 2015
2 parents 90eb043 + a9d9770 commit 744b23b
Show file tree
Hide file tree
Showing 12 changed files with 562 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions builtin/bins/provider-mysql/main.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
1 change: 1 addition & 0 deletions builtin/bins/provider-mysql/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
71 changes: 71 additions & 0 deletions builtin/providers/mysql/provider.go
Original file line number Diff line number Diff line change
@@ -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))
}
55 changes: 55 additions & 0 deletions builtin/providers/mysql/provider_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
174 changes: 174 additions & 0 deletions builtin/providers/mysql/resource_database.go
Original file line number Diff line number Diff line change
@@ -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 ""
}
Loading

0 comments on commit 744b23b

Please sign in to comment.