From 8623803dd71ec0b7eed2908f15a211adabb93b43 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Tue, 26 Jul 2016 12:19:39 -0600 Subject: [PATCH] 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 --- builtin/providers/mysql/provider.go | 2 + builtin/providers/mysql/resource_grant.go | 123 +++++++++++++++++ .../providers/mysql/resource_grant_test.go | 125 ++++++++++++++++++ builtin/providers/mysql/resource_user.go | 105 +++++++++++++++ builtin/providers/mysql/resource_user_test.go | 86 ++++++++++++ .../providers/mysql/r/grant.html.markdown | 52 ++++++++ .../docs/providers/mysql/r/user.html.markdown | 37 ++++++ website/source/layouts/mysql.erb | 6 + 8 files changed, 536 insertions(+) create mode 100644 builtin/providers/mysql/resource_grant.go create mode 100644 builtin/providers/mysql/resource_grant_test.go create mode 100644 builtin/providers/mysql/resource_user.go create mode 100644 builtin/providers/mysql/resource_user_test.go create mode 100644 website/source/docs/providers/mysql/r/grant.html.markdown create mode 100644 website/source/docs/providers/mysql/r/user.html.markdown diff --git a/builtin/providers/mysql/provider.go b/builtin/providers/mysql/provider.go index bd3401979..714e98d9f 100644 --- a/builtin/providers/mysql/provider.go +++ b/builtin/providers/mysql/provider.go @@ -50,6 +50,8 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "mysql_database": resourceDatabase(), + "mysql_user": resourceUser(), + "mysql_grant": resourceGrant(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/mysql/resource_grant.go b/builtin/providers/mysql/resource_grant.go new file mode 100644 index 000000000..332585011 --- /dev/null +++ b/builtin/providers/mysql/resource_grant.go @@ -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 +} diff --git a/builtin/providers/mysql/resource_grant_test.go b/builtin/providers/mysql/resource_grant_test.go new file mode 100644 index 000000000..d7541aee0 --- /dev/null +++ b/builtin/providers/mysql/resource_grant_test.go @@ -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"] +} +` diff --git a/builtin/providers/mysql/resource_user.go b/builtin/providers/mysql/resource_user.go new file mode 100644 index 000000000..c0969b7f3 --- /dev/null +++ b/builtin/providers/mysql/resource_user.go @@ -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 +} diff --git a/builtin/providers/mysql/resource_user_test.go b/builtin/providers/mysql/resource_user_test.go new file mode 100644 index 000000000..0d5b47cfb --- /dev/null +++ b/builtin/providers/mysql/resource_user_test.go @@ -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" +} +` diff --git a/website/source/docs/providers/mysql/r/grant.html.markdown b/website/source/docs/providers/mysql/r/grant.html.markdown new file mode 100644 index 000000000..43e2fa07c --- /dev/null +++ b/website/source/docs/providers/mysql/r/grant.html.markdown @@ -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. diff --git a/website/source/docs/providers/mysql/r/user.html.markdown b/website/source/docs/providers/mysql/r/user.html.markdown new file mode 100644 index 000000000..f70e274a3 --- /dev/null +++ b/website/source/docs/providers/mysql/r/user.html.markdown @@ -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. diff --git a/website/source/layouts/mysql.erb b/website/source/layouts/mysql.erb index ac2107197..2497b346c 100644 --- a/website/source/layouts/mysql.erb +++ b/website/source/layouts/mysql.erb @@ -16,6 +16,12 @@ > mysql_database + > + mysql_grant + + > + mysql_user +