diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e429005e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +GNUmakefile text eol=lf +*.sh text eol=lf diff --git a/GNUmakefile b/GNUmakefile index a7814d93..75edc9fe 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -2,7 +2,7 @@ TEST?=$$(go list ./... |grep -v 'vendor') GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) WEBSITE_REPO=github.com/hashicorp/terraform-website PKG_NAME=mysql -TERRAFORM_VERSION=0.14.7 +TERRAFORM_VERSION=1.11.3 TERRAFORM_OS=$(shell uname -s | tr A-Z a-z) TEST_USER=root TEST_PASSWORD=my-secret-pw @@ -20,15 +20,19 @@ bin/terraform: (cd $(CURDIR)/bin/ ; unzip terraform.zip) testacc: fmtcheck bin/terraform - PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout=120s + PATH="$(CURDIR)/bin:${PATH}" TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout=600s -acceptance: testversion5.6 testversion5.7 testversion8.0 testpercona5.7 testpercona8.0 testmariadb10.3 testmariadb10.8 testmariadb10.10 testtidb6.1.0 testtidb7.5.2 +acceptance: testversion5.6 testversion5.7 testversion8.0 testversion8.4.5 testpercona5.7 testpercona8.0 testmariadb10.3 testmariadb10.8 testmariadb10.10 testtidb6.1.0 testtidb7.5.2 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) + @MYSQLD_ARGS=""; \ + if [ "$$(echo $(MYSQL_VERSION) | awk -F. '{ if ($$1 > 8 || ($$1 == 8 && $$2 >= 4)) print 1; else print 0 }')" -eq 1 ]; then \ + MYSQLD_ARGS="mysqld --mysql-native-password=ON"; \ + fi; \ + docker run --rm --name test-mysql$(MYSQL_VERSION) -e MYSQL_ROOT_PASSWORD="$(TEST_PASSWORD)" -d -p $(MYSQL_PORT):3306 mysql:$(MYSQL_VERSION) $$MYSQLD_ARGS @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';" @@ -76,6 +80,7 @@ testmariadb: MYSQL_USERNAME="$(TEST_USER)" MYSQL_PASSWORD="$(TEST_PASSWORD)" MYSQL_ENDPOINT=127.0.0.1:$(MYSQL_PORT) $(MAKE) testacc -docker rm -f test-mariadb$(MYSQL_VERSION) + vet: @echo "go vet ." @go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \ diff --git a/mysql/resource_grant_test.go b/mysql/resource_grant_test.go index cc39f051..3c34c842 100644 --- a/mysql/resource_grant_test.go +++ b/mysql/resource_grant_test.go @@ -1033,7 +1033,7 @@ resource "mysql_grant" "test_procedure" { host = "%s" privileges = ["EXECUTE"] database = "PROCEDURE %s" - table = "%s" + table = "%s" } `, dbName, dbName, dbName, dbName, hostName, dbName, procedureName) } @@ -1150,7 +1150,7 @@ func TestAllowDuplicateUsersDifferentTables(t *testing.T) { user = "${mysql_user.test.user}" host = "${mysql_user.test.host}" database = "${mysql_database.test.name}" - table = "table1" + table = "table1" privileges = ["UPDATE", "SELECT"] } @@ -1215,7 +1215,7 @@ func TestDisallowDuplicateUsersSameTable(t *testing.T) { user = "${mysql_user.test.user}" host = "${mysql_user.test.host}" database = "${mysql_database.test.name}" - table = "table1" + table = "table1" privileges = ["UPDATE", "SELECT"] } @@ -1246,3 +1246,129 @@ func TestDisallowDuplicateUsersSameTable(t *testing.T) { }, }) } + +// TestModifyPrivileges explicitly verifies the correct and incorrect ways of modifying privileges. +// It tests adding privileges by augmenting the existing grant (correct way). +// It also tests that dynamic privileges configured on the global (`*`) database can coexist with grants on specific databases. +func TestModifyPrivileges(t *testing.T) { + dbName := fmt.Sprintf("tf-test-modify-%d", rand.Intn(100)) + roleName := fmt.Sprintf("TFRole-modify-%d", rand.Intn(100)) + userName := fmt.Sprintf("jdoe-modify-%s", dbName) + + onePrivilegeConfig := getGrantsSampleWithPrivileges(roleName, dbName, userName, `"SELECT"`) + twoPrivilegesConfig := getGrantsSampleWithPrivileges(roleName, dbName, userName, `"SELECT", "UPDATE"`) + additionalStaticPrivilegeConfig := twoPrivilegesConfig + getAdditionalGrantSample(dbName, `"INSERT"`) + threePrivilegesConfig := getGrantsSampleWithPrivileges(roleName, dbName, userName, `"SELECT", "UPDATE", "INSERT"`) + // Configuring dynamic privilege on global (`*`) database alongside specific database grants + additionalDynamicPrivilegeConfigFlushTables := threePrivilegesConfig + getAdditionalGrantSample("*", `"FLUSH_TABLES"`) + additionalDynamicPrivilegeConfigShowRoutine := threePrivilegesConfig + getAdditionalGrantSample("*", `"SHOW_ROUTINE"`) + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckSkipRds(t) + testAccPreCheckSkipMariaDB(t) + testAccPreCheckSkipNotMySQLVersionMin(t, "8.0.0") + testAccPreCheckSkipTiDB(t) + }, + ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccGrantCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGrantConfigNoGrant(dbName), + }, + { + Config: onePrivilegeConfig, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant", "SELECT", true, false), + testAccPrivilege("mysql_grant.grant", "UPDATE", false, false), + testAccPrivilege("mysql_grant.grant", "INSERT", false, false), + ), + }, + { + // Correct way: augment existing grant with additional privileges + Config: twoPrivilegesConfig, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant", "SELECT", true, false), + testAccPrivilege("mysql_grant.grant", "UPDATE", true, false), + testAccPrivilege("mysql_grant.grant", "INSERT", false, false), + ), + }, + { + // Incorrect way: create a new conflicting grant (expected to fail) + Config: additionalStaticPrivilegeConfig, + ExpectError: regexp.MustCompile("already has"), + }, + { + // Correct way: augment existing grant with additional privileges + Config: threePrivilegesConfig, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant", "SELECT", true, false), + testAccPrivilege("mysql_grant.grant", "UPDATE", true, false), + testAccPrivilege("mysql_grant.grant", "INSERT", true, false), + ), + }, + + // Testing coexistence of dynamic privilege on global (`*`) database with specific database grants + + { + Config: additionalDynamicPrivilegeConfigFlushTables, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant", "SELECT", true, false), + testAccPrivilege("mysql_grant.grant", "UPDATE", true, false), + testAccPrivilege("mysql_grant.grant", "INSERT", true, false), + testAccPrivilege("mysql_grant.additional_grant", "FLUSH_TABLES", true, false), + testAccPrivilege("mysql_grant.additional_grant", "SHOW_ROUTINE", false, false), + ), + }, + { + Config: additionalDynamicPrivilegeConfigShowRoutine, + Check: resource.ComposeTestCheckFunc( + testAccPrivilege("mysql_grant.grant", "SELECT", true, false), + testAccPrivilege("mysql_grant.grant", "UPDATE", true, false), + testAccPrivilege("mysql_grant.grant", "INSERT", true, false), + testAccPrivilege("mysql_grant.additional_grant", "FLUSH_TABLES", false, false), + testAccPrivilege("mysql_grant.additional_grant", "SHOW_ROUTINE", true, false), + ), + }, + }, + }) +} + +func getGrantsSampleWithPrivileges(roleName string, dbName string, userName string, privileges string) string { + return fmt.Sprintf(` + + resource "mysql_role" "role" { + name = "%s" + } + + resource "mysql_grant" "grant" { + role = "${mysql_role.role.name}" + database = "%s" + privileges = [%s] + } + + resource "mysql_user" "user" { + user = "%s" + host = "%%" + } + + resource "mysql_grant" "user_grant" { + user = "${mysql_user.user.user}" + host = "${mysql_user.user.host}" + database = "%s" + roles = ["${mysql_role.role.name}"] + } + + `, roleName, dbName, privileges, userName, dbName) +} + +func getAdditionalGrantSample(dbName string, privileges string) string { + return fmt.Sprintf(` + + resource "mysql_grant" "additional_grant" { + role = "${mysql_role.role.name}" + database = "%s" + privileges = [%s] + } + `, dbName, privileges) +} diff --git a/scripts/changelog-links.sh b/scripts/changelog-links.sh index 5fae45c7..8cf9ad74 100755 --- a/scripts/changelog-links.sh +++ b/scripts/changelog-links.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to # be Markdown links to the given github issues. diff --git a/scripts/gogetcookie.sh b/scripts/gogetcookie.sh index 26c63a64..2514c21a 100755 --- a/scripts/gogetcookie.sh +++ b/scripts/gogetcookie.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash touch ~/.gitcookies chmod 0600 ~/.gitcookies diff --git a/test/run-tests-cygwin.cmd b/test/run-tests-cygwin.cmd new file mode 100644 index 00000000..deca2b33 --- /dev/null +++ b/test/run-tests-cygwin.cmd @@ -0,0 +1,15 @@ + +set PATH=C:\cygwin64\bin;%PATH% +cd test1 +if errorlevel 1 goto exit +C:\cygwin64\bin\bash.exe ../test.sh +if errorlevel 1 goto exit +cd .. +if errorlevel 1 goto exit +cd test2 +if errorlevel 1 goto exit +C:\cygwin64\bin\bash.exe ../test.sh +:exit + + + diff --git a/test/run-tests.sh b/test/run-tests.sh new file mode 100644 index 00000000..719ef3b3 --- /dev/null +++ b/test/run-tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +cd test1 || exit 1 +../test.sh +cd .. || exit 1 + +cd test2 || exit 1 +../test.sh \ No newline at end of file diff --git a/test/test.sh b/test/test.sh new file mode 100644 index 00000000..b90ecb2e --- /dev/null +++ b/test/test.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +export TF_VAR_MYSQL_ROOT_USER="root" +export TF_VAR_MYSQL_ROOT_PASSWORD="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 20 | head -n 1)" +container1=$(docker run --rm --name test-mysql8.4.5-1 -e MYSQL_ROOT_PASSWORD=$TF_VAR_MYSQL_ROOT_PASSWORD -d -p 3307:3306 mysql:8.4.5 mysqld --mysql-native-password=ON) +if ! echo "$container1" | grep -Eq '^[0-9a-f]+$'; then + exit 1 +fi +container2=$(docker run --rm --name test-mysql8.4.5-2 -e MYSQL_ROOT_PASSWORD=$TF_VAR_MYSQL_ROOT_PASSWORD -d -p 3308:3306 mysql:8.4.5 mysqld --mysql-native-password=ON) +if ! echo "$container2" | grep -Eq '^[0-9a-f]+$'; then + exit 1 +fi + + +echo "Waiting for MySQL to become available..." +until docker exec "$container1" mysqladmin ping -h"localhost" -P3306 --silent; do + sleep 1 +done +until docker exec "$container2" mysqladmin ping -h"localhost" -P3306 --silent; do + sleep 1 +done + + +terraform init +terraform_code=$? +if [ $terraform_code -eq 0 ]; then + terraform apply -auto-approve + terraform_code=$? +fi + +docker container stop $container1 +docker container stop $container2 +exit $terraform_code \ No newline at end of file diff --git a/test/test1/test.tf b/test/test1/test.tf new file mode 100644 index 00000000..181585f4 --- /dev/null +++ b/test/test1/test.tf @@ -0,0 +1,121 @@ +terraform { + required_providers { + mysql = { + source = "petoju/mysql" + version = ">= 3.0.37" + } + } + required_version = ">= 1.11.4" +} + +variable "MYSQL_ROOT_USER" { + sensitive = true + type = string +} + +variable "MYSQL_ROOT_PASSWORD" { + sensitive = true + type = string +} + +# First provider + +provider "mysql" { + alias = "local1" + endpoint = "localhost:3307" + username = var.MYSQL_ROOT_USER + password = var.MYSQL_ROOT_PASSWORD +} + +resource "mysql_database" "test1" { + provider = mysql.local1 + name = "test-db" +} + + +resource "mysql_role" "analyst1" { + provider = mysql.local1 + name = "analyst" +} + +resource "mysql_grant" "grant_to_role_select1" { + provider = mysql.local1 + role = mysql_role.analyst1.name + database = mysql_database.test1.name + table = "*" + privileges = ["SELECT"] +} + +resource "mysql_grant" "grant_to_role_showroutine1" { + provider = mysql.local1 + role = mysql_role.analyst1.name + database = "*" + table = "*" + privileges = ["SHOW_ROUTINE"] +} + +resource "mysql_grant" "grant_to_user1" { + provider = mysql.local1 + user = mysql_user.user1.user + host = "%" + database = mysql_database.test1.name + roles = [mysql_role.analyst1.name] +} + + +resource "mysql_user" "user1" { + provider = mysql.local1 + user = "test-user" + host = "%" +} + +# Second provider + +provider "mysql" { + alias = "local2" + endpoint = "localhost:3308" + username = var.MYSQL_ROOT_USER + password = var.MYSQL_ROOT_PASSWORD +} + +resource "mysql_database" "test2" { + provider = mysql.local2 + name = "test-db" +} + + +resource "mysql_role" "analyst2" { + provider = mysql.local2 + name = "analyst" +} + +resource "mysql_grant" "grant_to_role_select2" { + provider = mysql.local2 + role = mysql_role.analyst2.name + database = mysql_database.test2.name + table = "*" + privileges = ["SELECT"] +} + +resource "mysql_grant" "grant_to_role_showroutine2" { + provider = mysql.local2 + role = mysql_role.analyst2.name + database = "*" + table = "*" + privileges = ["SHOW_ROUTINE"] +} + +resource "mysql_grant" "grant_to_user2" { + provider = mysql.local2 + user = mysql_user.user2.user + host = "%" + database = mysql_database.test2.name + roles = [mysql_role.analyst2.name] +} + + +resource "mysql_user" "user2" { + provider = mysql.local2 + user = "test-user" + host = "%" +} diff --git a/test/test2/test.tf b/test/test2/test.tf new file mode 100644 index 00000000..c8af7b2a --- /dev/null +++ b/test/test2/test.tf @@ -0,0 +1,118 @@ +terraform { + required_providers { + mysql = { + source = "petoju/mysql" + version = ">= 3.0.37" + } + } + required_version = ">= 1.11.4" +} + + +locals { + privileges = { + "1": ["SELECT", null, "*"], + "2": ["SHOW_ROUTINE", "*", "*"] + } +} + +variable "MYSQL_ROOT_USER" { + sensitive = true + type = string +} + +variable "MYSQL_ROOT_PASSWORD" { + sensitive = true + type = string +} + +# First provider + +provider "mysql" { + alias = "local1" + endpoint = "localhost:3307" + username = var.MYSQL_ROOT_USER + password = var.MYSQL_ROOT_PASSWORD +} + +resource "mysql_database" "test1" { + provider = mysql.local1 + name = "test-db" +} + + +resource "mysql_role" "analyst1" { + provider = mysql.local1 + name = "analyst" +} + +resource "mysql_grant" "grant_to_role1" { + for_each = local.privileges + provider = mysql.local1 + database = each.value[1] != null ? each.value[1] : mysql_database.test1.name + table = each.value[2] + privileges = toset([each.value[0]]) + role = mysql_role.analyst1.name +} + +resource "mysql_grant" "grant_to_user1" { + provider = mysql.local1 + user = mysql_user.user1.user + host = "%" + database = mysql_database.test1.name + roles = [mysql_role.analyst1.name] +} + + +resource "mysql_user" "user1" { + provider = mysql.local1 + user = "test-user" + host = "%" +} + + +# Second provider + +provider "mysql" { + alias = "local2" + endpoint = "localhost:3308" + username = var.MYSQL_ROOT_USER + password = var.MYSQL_ROOT_PASSWORD +} + +resource "mysql_database" "test2" { + provider = mysql.local2 + name = "test-db" +} + + +resource "mysql_role" "analyst2" { + provider = mysql.local2 + name = "analyst" +} + +resource "mysql_grant" "grant_to_role2" { + for_each = local.privileges + provider = mysql.local2 + database = each.value[1] != null ? each.value[1] : mysql_database.test2.name + table = each.value[2] + privileges = toset([each.value[0]]) + role = mysql_role.analyst2.name +} + +resource "mysql_grant" "grant_to_user2" { + provider = mysql.local2 + user = mysql_user.user2.user + host = "%" + database = mysql_database.test2.name + roles = [mysql_role.analyst2.name] +} + + +resource "mysql_user" "user2" { + provider = mysql.local2 + user = "test-user" + host = "%" +} + +