provider/mysql: user and grant resources (#7656)

* provider/mysql: User Resource

This commit introduces a mysql_user resource. It includes basic
functionality of adding a user@host along with a password.

* provider/mysql: Grant Resource

This commit introduces a mysql_grant resource. It can grant a set
of privileges to a user against a whole database.

* provider/mysql: Adding documentation for user and grant resources
This commit is contained in:
Joe Topjian 2016-07-26 12:19:39 -06:00 committed by Paul Stack
parent fcefa098b5
commit 8623803dd7
8 changed files with 536 additions and 0 deletions

View File

@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"mysql_database": resourceDatabase(),
"mysql_user": resourceUser(),
"mysql_grant": resourceGrant(),
},
ConfigureFunc: providerConfigure,

View File

@ -0,0 +1,123 @@
package mysql
import (
"fmt"
"log"
"strings"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceGrant() *schema.Resource {
return &schema.Resource{
Create: CreateGrant,
Update: nil,
Read: ReadGrant,
Delete: DeleteGrant,
Schema: map[string]*schema.Schema{
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"host": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "localhost",
},
"database": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"privileges": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"grant": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: false,
},
},
}
}
func CreateGrant(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
// create a comma-delimited string of privileges
var privileges string
var privilegesList []string
vL := d.Get("privileges").(*schema.Set).List()
for _, v := range vL {
privilegesList = append(privilegesList, v.(string))
}
privileges = strings.Join(privilegesList, ",")
stmtSQL := fmt.Sprintf("GRANT %s on %s.* TO '%s'@'%s'",
privileges,
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
if d.Get("grant").(bool) {
stmtSQL = " WITH GRANT OPTION"
}
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
user := fmt.Sprintf("%s@%s:%s", d.Get("user").(string), d.Get("host").(string), d.Get("database"))
d.SetId(user)
return ReadGrant(d, meta)
}
func ReadGrant(d *schema.ResourceData, meta interface{}) error {
// At this time, all attributes are supplied by the user
return nil
}
func DeleteGrant(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("REVOKE GRANT OPTION ON %s.* FROM '%s'@'%s'",
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
stmtSQL = fmt.Sprintf("REVOKE ALL ON %s.* FROM '%s'@'%s'",
d.Get("database").(string),
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err = conn.Query(stmtSQL)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,125 @@
package mysql
import (
"fmt"
"log"
"strings"
"testing"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccGrant(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGrantCheckDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccGrantConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccPrivilegeExists("mysql_grant.test", "SELECT"),
resource.TestCheckResourceAttr("mysql_grant.test", "user", "jdoe"),
resource.TestCheckResourceAttr("mysql_grant.test", "host", "example.com"),
resource.TestCheckResourceAttr("mysql_grant.test", "database", "foo"),
),
},
},
})
}
func testAccPrivilegeExists(rn string, privilege 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("grant id not set")
}
id := strings.Split(rs.Primary.ID, ":")
userhost := strings.Split(id[0], "@")
user := userhost[0]
host := userhost[1]
conn := testAccProvider.Meta().(mysqlc.Conn)
stmtSQL := fmt.Sprintf("SHOW GRANTS for '%s'@'%s'", user, host)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error reading grant: %s", err)
}
if len(rows) == 0 {
return fmt.Errorf("grant not found for '%s'@'%s'", user, host)
}
privilegeFound := false
for _, row := range rows {
log.Printf("Result Row: %s", row[0])
privIndex := strings.Index(string(row[0].([]byte)), privilege)
if privIndex != -1 {
privilegeFound = true
}
}
if !privilegeFound {
return fmt.Errorf("grant no found for '%s'@'%s'", user, host)
}
return nil
}
}
func testAccGrantCheckDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(mysqlc.Conn)
for _, rs := range s.RootModule().Resources {
if rs.Type != "mysql_grant" {
continue
}
id := strings.Split(rs.Primary.ID, ":")
userhost := strings.Split(id[0], "@")
user := userhost[0]
host := userhost[1]
stmtSQL := fmt.Sprintf("SHOW GRANTS for '%s'@'%s'", user, host)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
if mysqlErr, ok := err.(*mysqlc.Error); ok {
if mysqlErr.Code == mysqlc.ER_NONEXISTING_GRANT {
return nil
}
}
return fmt.Errorf("error reading grant: %s", err)
}
if len(rows) != 0 {
return fmt.Errorf("grant still exists for'%s'@'%s'", user, host)
}
}
return nil
}
const testAccGrantConfig_basic = `
resource "mysql_user" "test" {
user = "jdoe"
host = "example.com"
password = "password"
}
resource "mysql_grant" "test" {
user = "${mysql_user.test.user}"
host = "${mysql_user.test.host}"
database = "foo"
privileges = ["UPDATE", "SELECT"]
}
`

View File

@ -0,0 +1,105 @@
package mysql
import (
"fmt"
"log"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceUser() *schema.Resource {
return &schema.Resource{
Create: CreateUser,
Update: UpdateUser,
Read: ReadUser,
Delete: DeleteUser,
Schema: map[string]*schema.Schema{
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"host": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "localhost",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
},
}
}
func CreateUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("CREATE USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
password := d.Get("password").(string)
if password != "" {
stmtSQL = stmtSQL + fmt.Sprintf(" IDENTIFIED BY '%s'", password)
}
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
user := fmt.Sprintf("%s@%s", d.Get("user").(string), d.Get("host").(string))
d.SetId(user)
return nil
}
func UpdateUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
if d.HasChange("password") {
_, newpw := d.GetChange("password")
stmtSQL := fmt.Sprintf("ALTER USER '%s'@'%s' IDENTIFIED BY '%s'",
d.Get("user").(string),
d.Get("host").(string),
newpw.(string))
log.Println("Executing query:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err != nil {
return err
}
}
return nil
}
func ReadUser(d *schema.ResourceData, meta interface{}) error {
// At this time, all attributes are supplied by the user
return nil
}
func DeleteUser(d *schema.ResourceData, meta interface{}) error {
conn := meta.(mysqlc.Conn)
stmtSQL := fmt.Sprintf("DROP USER '%s'@'%s'",
d.Get("user").(string),
d.Get("host").(string))
log.Println("Executing statement:", stmtSQL)
_, _, err := conn.Query(stmtSQL)
if err == nil {
d.SetId("")
}
return err
}

View File

@ -0,0 +1,86 @@
package mysql
import (
"fmt"
"log"
"testing"
mysqlc "github.com/ziutek/mymysql/mysql"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccUser(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccUserCheckDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccUserConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccUserExists("mysql_user.test"),
resource.TestCheckResourceAttr("mysql_user.test", "user", "jdoe"),
resource.TestCheckResourceAttr("mysql_user.test", "host", "example.com"),
resource.TestCheckResourceAttr("mysql_user.test", "password", "password"),
),
},
},
})
}
func testAccUserExists(rn 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("user id not set")
}
conn := testAccProvider.Meta().(mysqlc.Conn)
stmtSQL := fmt.Sprintf("SELECT count(*) from mysql.user where CONCAT(user, '@', host) = '%s'", rs.Primary.ID)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error reading user: %s", err)
}
if len(rows) != 1 {
return fmt.Errorf("expected 1 row reading user but got %d", len(rows))
}
return nil
}
}
func testAccUserCheckDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(mysqlc.Conn)
for _, rs := range s.RootModule().Resources {
if rs.Type != "mysql_user" {
continue
}
stmtSQL := fmt.Sprintf("SELECT user from mysql.user where CONCAT(user, '@', host) = '%s'", rs.Primary.ID)
log.Println("Executing statement:", stmtSQL)
rows, _, err := conn.Query(stmtSQL)
if err != nil {
return fmt.Errorf("error issuing query: %s", err)
}
if len(rows) != 0 {
return fmt.Errorf("user still exists after destroy")
}
}
return nil
}
const testAccUserConfig_basic = `
resource "mysql_user" "test" {
user = "jdoe"
host = "example.com"
password = "password"
}
`

View File

@ -0,0 +1,52 @@
---
layout: "mysql"
page_title: "MySQL: mysql_grant"
sidebar_current: "docs-mysql-resource-grant"
description: |-
Creates and manages privileges given to a user on a MySQL server
---
# mysql\_grant
The ``mysql_grant`` resource creates and manages privileges given to
a user on a MySQL server.
## Example Usage
```
resource "mysql_user" "jdoe" {
user = "jdoe"
host = "example.com"
password = "password"
}
resource "mysql_grant" "jdoe" {
user = "${mysql_user.jdoe.user}"
host = "${mysql_user.jdoe.host}"
database = "app"
privileges = ["SELECT", "UPDATE"]
}
```
## Argument Reference
The following arguments are supported:
* `user` - (Required) The name of the user.
* `host` - (Optional) The source host of the user. Defaults to "localhost".
* `database` - (Required) The database to grant privileges on. At this time,
privileges are given to all tables on the database (`mydb.*`).
* `privileges` - (Required) A list of privileges to grant to the user. Refer
to a list of privileges (such as
[here](https://dev.mysql.com/doc/refman/5.5/en/grant.html)) for applicable
privileges.
* `grant` - (Optional) Whether to also give the user privileges to grant
the same privileges to other users.
## Attributes Reference
No further attributes are exported.

View File

@ -0,0 +1,37 @@
---
layout: "mysql"
page_title: "MySQL: mysql_user"
sidebar_current: "docs-mysql-resource-user"
description: |-
Creates and manages a user on a MySQL server.
---
# mysql\_user
The ``mysql_user`` resource creates and manages a user on a MySQL
server.
## Example Usage
```
resource "mysql_user" "jdoe" {
user = "jdoe"
host = "example.com"
password = "password"
}
```
## Argument Reference
The following arguments are supported:
* `user` - (Required) The name of the user.
* `host` - (Optional) The source host of the user. Defaults to "localhost".
* `password` - (Optional) The password of the user. The value of this
argument is plain-text so make sure to secure where this is defined.
## Attributes Reference
No further attributes are exported.

View File

@ -16,6 +16,12 @@
<li<%= sidebar_current("docs-mysql-resource-database") %>>
<a href="/docs/providers/mysql/r/database.html">mysql_database</a>
</li>
<li<%= sidebar_current("docs-mysql-resource-grant") %>>
<a href="/docs/providers/mysql/r/grant.html">mysql_grant</a>
</li>
<li<%= sidebar_current("docs-mysql-resource-user") %>>
<a href="/docs/providers/mysql/r/user.html">mysql_user</a>
</li>
</ul>
</li>
</ul>