Skip to content

Commit

Permalink
Merge branch 'master' of /Users/jake/terraform
Browse files Browse the repository at this point in the history
  • Loading branch information
grubernaut committed Jun 6, 2017
2 parents 791f197 + bde5def commit 4b2c10a
Show file tree
Hide file tree
Showing 8 changed files with 924 additions and 0 deletions.
116 changes: 116 additions & 0 deletions mysql/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package mysql

import (
"fmt"
"strings"

"github.com/hashicorp/go-version"
mysqlc "github.com/ziutek/mymysql/mysql"
mysqlts "github.com/ziutek/mymysql/thrsafe"

"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)

type providerConfiguration struct {
Conn mysqlc.Conn
ServerVersion *version.Version
}

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),
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if value == "" {
errors = append(errors, fmt.Errorf("Endpoint must not be an empty string"))
}

return
},
},

"username": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("MYSQL_USERNAME", nil),
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if value == "" {
errors = append(errors, fmt.Errorf("Username must not be an empty string"))
}

return
},
},

"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("MYSQL_PASSWORD", nil),
},
},

ResourcesMap: map[string]*schema.Resource{
"mysql_database": resourceDatabase(),
"mysql_user": resourceUser(),
"mysql_grant": resourceGrant(),
},

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 len(endpoint) > 0 && endpoint[0] == '/' {
proto = "unix"
}

// mysqlts is the thread-safe implementation of mymysql, so we can
// safely re-use the same connection between multiple parallel
// operations.
conn := mysqlts.New(proto, "", endpoint, username, password)

err := conn.Connect()
if err != nil {
return nil, err
}

ver, err := serverVersion(conn)
if err != nil {
return nil, err
}

return &providerConfiguration{
Conn: conn,
ServerVersion: ver,
}, nil
}

var identQuoteReplacer = strings.NewReplacer("`", "``")

func quoteIdentifier(in string) string {
return fmt.Sprintf("`%s`", identQuoteReplacer.Replace(in))
}

func serverVersion(conn mysqlc.Conn) (*version.Version, error) {
rows, _, err := conn.Query("SELECT VERSION()")
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, fmt.Errorf("SELECT VERSION() returned an empty set")
}

return version.NewVersion(rows[0].Str(0))
}
55 changes: 55 additions & 0 deletions 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 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.(*providerConfiguration).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.(*providerConfiguration).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.(*providerConfiguration).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.(*providerConfiguration).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 4b2c10a

Please sign in to comment.