Skip to content

Commit

Permalink
provider/mysql: user and grant resources (#7656)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jtopjian authored and stack72 committed Jul 26, 2016
1 parent a20c711 commit 0b890b8
Show file tree
Hide file tree
Showing 5 changed files with 441 additions and 0 deletions.
2 changes: 2 additions & 0 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider {

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

ConfigureFunc: providerConfigure,
Expand Down
123 changes: 123 additions & 0 deletions resource_grant.go
Original file line number Diff line number Diff line change
@@ -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
}
125 changes: 125 additions & 0 deletions resource_grant_test.go
Original file line number Diff line number Diff line change
@@ -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"]
}
`
105 changes: 105 additions & 0 deletions resource_user.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 0b890b8

Please sign in to comment.