diff --git a/.gitignore b/.gitignore index 1b116c58..544851cf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,14 @@ website/node_modules *~ .*.swp .idea +/.vscode *.iml *.test *.iml +# goreleaser +/dist + website/vendor # Test exclusions diff --git a/GNUmakefile b/GNUmakefile index 4e02f17c..7526875e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -3,6 +3,7 @@ GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) WEBSITE_REPO=github.com/hashicorp/terraform-website PKG_NAME=mysql TERRAFORM_VERSION=0.14.7 +TERRAFORM_OS=$(shell uname -s | tr A-Z a-z) TEST_USER=root TEST_PASSWORD=my-secret-pw @@ -17,21 +18,22 @@ test: fmtcheck xargs -t -n4 go test $(TESTARGS) -timeout=60s -parallel=4 bin/terraform: - mkdir "$(CURDIR)/bin" - curl https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION)/terraform_$(TERRAFORM_VERSION)_linux_amd64.zip > $(CURDIR)/bin/terraform.zip + mkdir -p "$(CURDIR)/bin" + curl -sfL https://releases.hashicorp.com/terraform/$(TERRAFORM_VERSION)/terraform_$(TERRAFORM_VERSION)_$(TERRAFORM_OS)_amd64.zip > $(CURDIR)/bin/terraform.zip (cd $(CURDIR)/bin/ ; unzip terraform.zip) testacc: fmtcheck bin/terraform PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout=60s -acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 +acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testtidb6.1.0 testversion%: $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=33$(shell echo "$*" | tr -d '.') testversion testversion: -docker run --rm --name test-mysql$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 mysql:$(MYSQL_VERSION) - @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1'; do echo 'Waiting for MySQL...'; sleep 1; done + @echo 'Waiting for MySQL...' + @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" -mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so';" MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc docker rm -f test-mysql$(MYSQL_VERSION) @@ -41,11 +43,21 @@ testpercona%: testpercona: -docker run --rm --name test-percona$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 percona:$(MYSQL_VERSION) - @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1'; do echo 'Waiting for Percona...'; sleep 1; done + @echo 'Waiting for Percona...' + @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" -mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -p"$(TEST_PASSWORD)" -e "INSTALL PLUGIN mysql_no_login SONAME 'mysql_no_login.so';" MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc docker rm -f test-percona$(MYSQL_VERSION) +testtidb%: + $(MAKE) MYSQL_VERSION=$* MYSQL_PORT=34$(shell echo "$*" | tr -d '.') testtidb + +testtidb: + -docker run --rm --name test-tidb$(MYSQL_VERSION) -d -p $(MYSQL_PORT):4000 pingcap/tidb:v$(MYSQL_VERSION) + @echo 'Waiting for TiDB...' + @while ! mysql -h 127.0.0.1 -P $(MYSQL_PORT) -u "$(TEST_USER)" -e 'SELECT 1' >/dev/null 2>&1; do printf '.'; sleep 1; done ; echo ; echo "Connected!" + MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc + docker rm -f test-tidb$(MYSQL_VERSION) vet: @echo "go vet ." @@ -88,4 +100,3 @@ endif @$(MAKE) -C $(GOPATH)/src/$(WEBSITE_REPO) website-provider PROVIDER_PATH=$(shell pwd) PROVIDER_NAME=$(PKG_NAME) .PHONY: build test testacc vet fmt fmtcheck errcheck vendor-status test-compile website website-test - diff --git a/mysql/provider.go b/mysql/provider.go index 159ccbef..05e69cff 100644 --- a/mysql/provider.go +++ b/mysql/provider.go @@ -131,12 +131,13 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ - "mysql_database": resourceDatabase(), - "mysql_grant": resourceGrant(), - "mysql_role": resourceRole(), - "mysql_user": resourceUser(), - "mysql_user_password": resourceUserPassword(), - "mysql_sql": resourceSql(), + "mysql_database": resourceDatabase(), + "mysql_global_variable": resourceGlobalVariable(), + "mysql_grant": resourceGrant(), + "mysql_role": resourceRole(), + "mysql_sql": resourceSql(), + "mysql_user_password": resourceUserPassword(), + "mysql_user": resourceUser(), }, ConfigureFunc: providerConfigure, diff --git a/mysql/provider_test.go b/mysql/provider_test.go index b61c4322..6c254f4c 100644 --- a/mysql/provider_test.go +++ b/mysql/provider_test.go @@ -3,6 +3,7 @@ package mysql import ( "context" "os" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -65,3 +66,20 @@ func testAccPreCheck(t *testing.T) { t.Fatal(err) } } + +func testAccPreCheckSkipTiDB(t *testing.T) { + testAccPreCheck(t) + db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + return + } + + currentVersionString, err := serverVersionString(db) + if err != nil { + return + } + + if strings.Contains(currentVersionString, "TiDB") { + t.Skip("Skip on TiDB") + } +} diff --git a/mysql/resource_database_test.go b/mysql/resource_database_test.go index e1f650a3..6b48a2c2 100644 --- a/mysql/resource_database_test.go +++ b/mysql/resource_database_test.go @@ -14,7 +14,7 @@ import ( func TestAccDatabase(t *testing.T) { dbName := "terraform_acceptance_test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckSkipTiDB(t) }, Providers: testAccProviders, CheckDestroy: testAccDatabaseCheckDestroy(dbName), Steps: []resource.TestStep{ @@ -39,7 +39,7 @@ func TestAccDatabase_collationChange(t *testing.T) { resourceName := "mysql_database.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckSkipTiDB(t) }, Providers: testAccProviders, CheckDestroy: testAccDatabaseCheckDestroy(dbName), Steps: []resource.TestStep{ diff --git a/mysql/resource_global_variable.go b/mysql/resource_global_variable.go new file mode 100644 index 00000000..f3a78440 --- /dev/null +++ b/mysql/resource_global_variable.go @@ -0,0 +1,106 @@ +package mysql + +import ( + "database/sql" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGlobalVariable() *schema.Resource { + return &schema.Resource{ + Create: CreateGlobalVariable, + Read: ReadGlobalVariable, + Update: UpdateGlobalVariable, + Delete: DeleteGlobalVariable, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func CreateGlobalVariable(d *schema.ResourceData, meta interface{}) error { + db := meta.(*MySQLConfiguration).Db + + name := d.Get("name").(string) + value := d.Get("value").(string) + + sql := fmt.Sprintf("SET GLOBAL %s = %s", quoteIdentifier(name), value) + log.Printf("[DEBUG] SQL: %s", sql) + + _, err := db.Exec(sql) + if err != nil { + return fmt.Errorf("error setting value: %s", err) + } + + d.SetId(name) + + return nil +} + +func ReadGlobalVariable(d *schema.ResourceData, meta interface{}) error { + db := meta.(*MySQLConfiguration).Db + + stmt, err := db.Prepare("SHOW GLOBAL VARIABLES WHERE VARIABLE_NAME = ?") + if err != nil { + return fmt.Errorf("error during prepare statement for global variable: %s", err) + } + + var name, value string + err = stmt.QueryRow(d.Id()).Scan(&name, &value) + + if err != nil && err != sql.ErrNoRows { + d.SetId("") + return fmt.Errorf("error during show global variables: %s", err) + } + + d.Set("name", name) + d.Set("value", value) + + return nil +} + +func UpdateGlobalVariable(d *schema.ResourceData, meta interface{}) error { + db := meta.(*MySQLConfiguration).Db + + name := d.Get("name").(string) + value := d.Get("value").(string) + + sql := fmt.Sprintf("SET GLOBAL %s = %s", quoteIdentifier(name), value) + log.Printf("[DEBUG] SQL: %s", sql) + + _, err := db.Exec(sql) + if err != nil { + return fmt.Errorf("error update value: %s", err) + } + return ReadGlobalVariable(d, meta) +} + +func DeleteGlobalVariable(d *schema.ResourceData, meta interface{}) error { + db := meta.(*MySQLConfiguration).Db + name := d.Get("name").(string) + + sql := fmt.Sprintf("SET GLOBAL %s = DEFAULT", quoteIdentifier(name)) + log.Printf("[DEBUG] SQL: %s", sql) + + _, err := db.Exec(sql) + if err != nil { + log.Printf("[WARN] Variable_name (%s) not found; removing from state", d.Id()) + d.SetId("") + return nil + } + + return nil +} diff --git a/mysql/resource_global_variable_test.go b/mysql/resource_global_variable_test.go new file mode 100644 index 00000000..26f851ca --- /dev/null +++ b/mysql/resource_global_variable_test.go @@ -0,0 +1,93 @@ +package mysql + +import ( + "database/sql" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccGlobalVar_basic(t *testing.T) { + varName := "max_connections" + resourceName := "mysql_global_variable.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGlobalVarCheckDestroy(varName), + Steps: []resource.TestStep{ + { + Config: testAccGlobalVarConfig_basic(varName), + Check: resource.ComposeTestCheckFunc( + testAccGlobalVarExists(varName), + resource.TestCheckResourceAttr(resourceName, "name", varName), + ), + }, + }, + }) +} + +func testAccGlobalVarExists(varName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + return err + } + + count, err := testAccGetGlobalVar(varName, db) + + if err != nil { + return err + } + + if count == 1 { + return nil + } + + return fmt.Errorf("variable '%s' not found", varName) + } +} + +func testAccGetGlobalVar(varName string, db *sql.DB) (int, error) { + stmt, err := db.Prepare("SHOW GLOBAL VARIABLES WHERE VARIABLE_NAME = ?") + if err != nil { + return 0, err + } + + var name string + var value int + err = stmt.QueryRow(varName).Scan(&name, &value) + + if err != nil && err != sql.ErrNoRows { + return 0, err + } + + return value, nil +} + +func testAccGlobalVarCheckDestroy(varName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + db, err := connectToMySQL(testAccProvider.Meta().(*MySQLConfiguration)) + if err != nil { + return err + } + + count, err := testAccGetGlobalVar(varName, db) + if count == 1 { + return fmt.Errorf("Global variable '%s' still has non default value", varName) + } + + return nil + } +} + +func testAccGlobalVarConfig_basic(varName string) string { + return fmt.Sprintf(` +resource "mysql_global_variable" "test" { + name = "%s" + value = 1 +} +`, varName) +} diff --git a/mysql/resource_grant_test.go b/mysql/resource_grant_test.go index f9d25c85..e6176cac 100644 --- a/mysql/resource_grant_test.go +++ b/mysql/resource_grant_test.go @@ -143,7 +143,7 @@ func TestAccDifferentHosts(t *testing.T) { func TestAccGrantComplex(t *testing.T) { dbName := fmt.Sprintf("tf-test-%d", rand.Intn(100)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckSkipTiDB(t) }, Providers: testAccProviders, CheckDestroy: testAccGrantCheckDestroy, Steps: []resource.TestStep{ diff --git a/mysql/resource_user_test.go b/mysql/resource_user_test.go index e0dddd43..3814a47d 100644 --- a/mysql/resource_user_test.go +++ b/mysql/resource_user_test.go @@ -52,7 +52,7 @@ func TestAccUser_basic(t *testing.T) { func TestAccUser_auth(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckSkipTiDB(t) }, Providers: testAccProviders, CheckDestroy: testAccUserCheckDestroy, Steps: []resource.TestStep{ diff --git a/website/docs/r/global_variable.html.markdown b/website/docs/r/global_variable.html.markdown new file mode 100644 index 00000000..42c2fa08 --- /dev/null +++ b/website/docs/r/global_variable.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "mysql" +page_title: "MySQL: mysql_global_variable" +sidebar_current: "docs-mysql-resource-global-variable" +description: |- + Manages a global variables on a MySQL server. +--- + +# mysql\_global\_variable + +The ``mysql_global_variable`` resource manages a global variables on a MySQL +server. + +~> **Note on MySQL:** MySQL global variables are [not persistent](https://dev.mysql.com/doc/refman/5.7/en/set-variable.html) + +~> **Note on TiDB:** TiDB global variables are [persistent](https://docs.pingcap.com/tidb/v5.4/sql-statement-set-variable#mysql-compatibility) + +~> **Note about `destroy`:** `destroy` will try assign `DEFAULT` value for global variable. + Unfortunately not every variable support this. + +## Example Usage + +```hcl +resource "mysql_global_variable" "max_connections" { + name = "max_connections" + value = "100" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the global variable. +* `value` - (Required) The value of the global variable. + +## Attributes Reference + +No further attributes are exported. + +## Import + +Global variable can be imported using global variable name. + +```shell +$ terraform import mysql_global_variable.max_connections max_connections +```