Skip to content

Commit

Permalink
Switch to lazy connections to MySQL and add TLS configuration support…
Browse files Browse the repository at this point in the history
… to the provider (hashicorp#43)

* Switch to using the Golang struct and DSN formatter from the MySQL module.
* Add the TLS option to the provider docs.
* Add a retry to the MySQL connection logic. This lets the server be a little lazy in booting up without killing the provisioning processes that require it.
* Switch to detecting the version in a way that works with MySQL and MariaDB.
* Add the ability to import databases.
  • Loading branch information
Joe Stump authored Oct 22, 2018
1 parent 5c63266 commit e3dc61a
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 145 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
example.tf
terraform.tfplan
terraform.tfstate
terraform-provider-mysql
bin/
modules-dev/
/pkg/
Expand Down
24 changes: 14 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
## 1.1.1 (Unreleased)
## 1.5.0 (Unreleased)

IMPROVEMENTS:
BUG FIXES:

* `resource/user`: Added the `tls_option` attribute, which allows to restrict
the MySQL users to a specific MySQL-TLS-Encryption. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/40))
* Lazily connect to MySQL servers. ([#43](https://github.com/terraform-providers/terraform-provider-mysql/issues/43))
* Add retries to MySQL server connection logic. ([#43](https://github.com/terraform-providers/terraform-provider-mysql/issues/43))
* Migrated to Go modules for dependencies and `vendor/` management.

* `resource/gant`: Added the `tls_option` attribute, which allows to restrict
the MySQL grant to a specific MySQL-TLS-Encryption. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/40))
IMPROVEMENTS:

* Provider now has a `tls` option that configures TSL for server connections. ([#43](https://github.com/terraform-providers/terraform-provider-mysql/issues/43))
* `r/mysql_user`: Added the `tls_option` attribute, which allows to restrict the MySQL users to a specific MySQL-TLS-Encryption. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/40))
* `r/mysql_grant`: Added the `tls_option` attribute, which allows to restrict the MySQL grant to a specific MySQL-TLS-Encryption. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/40))
* `r/mysql_grant`: Added a `table` argument that allows `GRANT` statements to be scoped to a single table.

## 1.1.0 (March 28, 2018)

IMPROVEMENTS:

* `resource/user`: Added the `auth_plugin` attribute, which allows for the use
of authentication plugins when creating MySQL users. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/26))
* `resource/user`: Added the `auth_plugin` attribute, which allows for the use of authentication plugins when creating MySQL users. ([#26](https://github.com/terraform-providers/terraform-provider-mysql/issues/26))

## 1.0.1 (January 03, 2018)

Expand All @@ -25,9 +29,9 @@ BUG FIXES:

UPGRADE NOTES:

* This provider is now using a different underlying library to access MySQL (See [[#16](https://github.com/terraform-providers/terraform-provider-mysql/issues/16)]). This should be a drop-in replacement for all of the functionality exposed by this provider, but just in case it is suggested to test cautiously after upgrading (review plans before applying, etc) in case of any edge-cases in interactions with specific versions of MySQL.
* This provider is now using a different underlying library to access MySQL (See [#16](https://github.com/terraform-providers/terraform-provider-mysql/issues/16)). This should be a drop-in replacement for all of the functionality exposed by this provider, but just in case it is suggested to test cautiously after upgrading (review plans before applying, etc) in case of any edge-cases in interactions with specific versions of MySQL.

ENHANCEMENTS:
IMPROVEMENTS:

* `mysql_user` now has a `plaintext_password` argument which stores its value in state as an _unsalted_ hash. This deprecates `password`, which stores its value in the state in cleartext. Since the hash is unsalted, some care is still warranted to secure the state, and a strong password should be used to reduce the chance of a successful brute-force attack on the hash. ([#9](https://github.com/terraform-providers/terraform-provider-mysql/issues/9))

Expand Down
94 changes: 61 additions & 33 deletions mysql/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import (
"database/sql"
"fmt"
"strings"
"time"

_ "github.com/go-sql-driver/mysql"
"github.com/go-sql-driver/mysql"
"github.com/hashicorp/go-version"

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

type providerConfiguration struct {
DB *sql.DB
ServerVersion *version.Version
type MySQLConfiguration struct {
Config *mysql.Config
}

func Provider() terraform.ResourceProvider {
Expand All @@ -38,21 +39,26 @@ func Provider() terraform.ResourceProvider {
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),
},

"tls": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("MYSQL_TLS_CONFIG", "false"),
/*
ValidateFunc: validation.StringInSlice([]string{
"true",
"false",
"skip-verify",
}, false),
*/
},
},

ResourcesMap: map[string]*schema.Resource{
Expand All @@ -67,30 +73,23 @@ func Provider() terraform.ResourceProvider {

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

// database/sql is the thread-safe by default, so we can
// safely re-use the same handle between multiple parallel
// operations.

dataSourceName := fmt.Sprintf("%s:%s@%s(%s)/", username, password, proto, endpoint)
db, err := sql.Open("mysql", dataSourceName)

ver, err := serverVersion(db)
if err != nil {
return nil, err
conf := mysql.Config{
User: d.Get("username").(string),
Passwd: d.Get("password").(string),
Net: proto,
Addr: endpoint,
TLSConfig: d.Get("tls").(string),
}

return &providerConfiguration{
DB: db,
ServerVersion: ver,
return &MySQLConfiguration{
Config: &conf,
}, nil
}

Expand All @@ -101,15 +100,44 @@ func quoteIdentifier(in string) string {
}

func serverVersion(db *sql.DB) (*version.Version, error) {
rows, err := db.Query("SELECT VERSION()")
var versionString string
err := db.QueryRow("SELECT @@GLOBAL.innodb_version").Scan(&versionString)
if err != nil {
return nil, err
}
if !rows.Next() {
return nil, fmt.Errorf("SELECT VERSION() returned an empty set")
}

var versionString string
rows.Scan(&versionString)
return version.NewVersion(versionString)
}

func connectToMySQL(conf *mysql.Config) (*sql.DB, error) {
dsn := conf.FormatDSN()
var db *sql.DB
var err error

// When provisioning a database server there can often be a lag between
// when Terraform thinks it's available and when it is actually available.
// This is particularly acute when provisioning a server and then immediately
// trying to provision a database on it.
retryError := resource.Retry(5*time.Minute, func() *resource.RetryError {
db, err = sql.Open("mysql", dsn)
if err != nil {
return resource.RetryableError(err)
}

// The Go SDK for MySQL doesn't actually connect until a query is ran.
// This forces a simple query to run, which runs a connect, which lets
// the retry logic do its thing.
_, err = serverVersion(db)
if err != nil {
return resource.RetryableError(err)
}

return nil
})

if retryError != nil {
return nil, fmt.Errorf("Could not connect to server: %s", retryError)
}

return db, nil
}
37 changes: 26 additions & 11 deletions mysql/resource_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func resourceDatabase() *schema.Resource {
Update: UpdateDatabase,
Read: ReadDatabase,
Delete: DeleteDatabase,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Expand All @@ -45,37 +47,46 @@ func resourceDatabase() *schema.Resource {
}

func CreateDatabase(d *schema.ResourceData, meta interface{}) error {
db := meta.(*providerConfiguration).DB
db, err := connectToMySQL(meta.(*MySQLConfiguration).Config)
if err != nil {
return err
}

stmtSQL := databaseConfigSQL("CREATE", d)
log.Println("Executing statement:", stmtSQL)

_, err := db.Exec(stmtSQL)
_, err = db.Exec(stmtSQL)
if err != nil {
return err
}

d.SetId(d.Get("name").(string))

return nil
return ReadDatabase(d, meta)
}

func UpdateDatabase(d *schema.ResourceData, meta interface{}) error {
db := meta.(*providerConfiguration).DB
db, err := connectToMySQL(meta.(*MySQLConfiguration).Config)
if err != nil {
return err
}

stmtSQL := databaseConfigSQL("ALTER", d)
log.Println("Executing statement:", stmtSQL)

_, err := db.Exec(stmtSQL)
_, err = db.Exec(stmtSQL)
if err != nil {
return err
}

return nil
return ReadDatabase(d, meta)
}

func ReadDatabase(d *schema.ResourceData, meta interface{}) error {
db := meta.(*providerConfiguration).DB
db, err := connectToMySQL(meta.(*MySQLConfiguration).Config)
if err != nil {
return err
}

// This is kinda flimsy-feeling, since it depends on the formatting
// of the SHOW CREATE DATABASE output... but this data doesn't seem
Expand All @@ -87,7 +98,7 @@ func ReadDatabase(d *schema.ResourceData, meta interface{}) error {

log.Println("Executing query:", stmtSQL)
var createSQL, _database string
err := db.QueryRow(stmtSQL).Scan(&_database, &createSQL)
err = db.QueryRow(stmtSQL).Scan(&_database, &createSQL)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == unknownDatabaseErrCode {
Expand Down Expand Up @@ -116,20 +127,24 @@ func ReadDatabase(d *schema.ResourceData, meta interface{}) error {
}
}

d.Set("name", name)
d.Set("default_character_set", defaultCharset)
d.Set("default_collation", defaultCollation)

return nil
}

func DeleteDatabase(d *schema.ResourceData, meta interface{}) error {
db := meta.(*providerConfiguration).DB
db, err := connectToMySQL(meta.(*MySQLConfiguration).Config)
if err != nil {
return err
}

name := d.Id()
stmtSQL := "DROP DATABASE " + quoteIdentifier(name)
log.Println("Executing statement:", stmtSQL)

_, err := db.Exec(stmtSQL)
_, err = db.Exec(stmtSQL)
if err == nil {
d.SetId("")
}
Expand Down
49 changes: 26 additions & 23 deletions mysql/resource_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,36 @@ func TestAccDatabase_collationChange(t *testing.T) {
collation1 := "latin1_bin"
collation2 := "utf8_general_ci"

resourceName := "mysql_database.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccDatabaseCheckDestroy(dbName),
Steps: []resource.TestStep{
resource.TestStep{
{
Config: testAccDatabaseConfig_full(dbName, charset1, collation1),
Check: resource.ComposeTestCheckFunc(
testAccDatabaseCheck_full(
"mysql_database.test", dbName, charset1, collation1,
),
testAccDatabaseCheck_full("mysql_database.test", dbName, charset1, collation1),
),
},
resource.TestStep{
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
PreConfig: func() {
db := testAccProvider.Meta().(*providerConfiguration).DB
db.Query(fmt.Sprintf("ALTER DATABASE %s CHARACTER SET %s COLLATE %s", dbName, charset2, collation2))
db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration).Config)
if err != nil {
return
}

db.Exec(fmt.Sprintf("ALTER DATABASE %s CHARACTER SET %s COLLATE %s", dbName, charset2, collation2))
},
Config: testAccDatabaseConfig_full(dbName, charset1, collation1),
Check: resource.ComposeTestCheckFunc(
testAccDatabaseCheck_full(
"mysql_database.test", dbName, charset1, collation1,
),
testAccDatabaseCheck_full(resourceName, dbName, charset1, collation1),
),
},
},
Expand All @@ -81,18 +88,15 @@ func testAccDatabaseCheck_full(rn string, name string, charset string, collation
return fmt.Errorf("database id not set")
}

db := testAccProvider.Meta().(*providerConfiguration).DB
rows, err := db.Query(fmt.Sprintf("SHOW CREATE DATABASE %s", name))
db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration).Config)
if err != nil {
return fmt.Errorf("error reading database: %s", err)
return err
}
defer rows.Close()

rows.Next()
var _name, createSQL string
err = rows.Scan(&_name, &createSQL)
err = db.QueryRow(fmt.Sprintf("SHOW CREATE DATABASE %s", name)).Scan(&_name, &createSQL)
if err != nil {
return fmt.Errorf("error scanning create statement: %s", err)
return fmt.Errorf("error reading database: %s", err)
}

if strings.Index(createSQL, fmt.Sprintf("CHARACTER SET %s", charset)) == -1 {
Expand All @@ -102,20 +106,19 @@ func testAccDatabaseCheck_full(rn string, name string, charset string, collation
return fmt.Errorf("database default collation isn't %s", collation)
}

if rows.Next() {
return fmt.Errorf("expected 1 row reading database, but got more")
}

return nil
}
}

func testAccDatabaseCheckDestroy(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
db := testAccProvider.Meta().(*providerConfiguration).DB
db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration).Config)
if err != nil {
return err
}

var _name, createSQL string
err := db.QueryRow(fmt.Sprintf("SHOW CREATE DATABASE %s", name)).Scan(&_name, &createSQL)
err = db.QueryRow(fmt.Sprintf("SHOW CREATE DATABASE %s", name)).Scan(&_name, &createSQL)
if err == nil {
return fmt.Errorf("database still exists after destroy")
}
Expand Down
Loading

0 comments on commit e3dc61a

Please sign in to comment.