| package pg |
| |
| // Create the test database: createdb terraform_backend_pg_test |
| // TF_ACC=1 GO111MODULE=on go test -v -mod=vendor -timeout=2m -parallel=4 github.com/hashicorp/terraform/backend/remote-state/pg |
| |
| import ( |
| "database/sql" |
| "fmt" |
| "os" |
| "testing" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/states/remote" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/lib/pq" |
| _ "github.com/lib/pq" |
| ) |
| |
| // Function to skip a test unless in ACCeptance test mode. |
| // |
| // A running Postgres server identified by env variable |
| // DATABASE_URL is required for acceptance tests. |
| func testACC(t *testing.T) { |
| skip := os.Getenv("TF_ACC") == "" |
| if skip { |
| t.Log("pg backend tests require setting TF_ACC") |
| t.Skip() |
| } |
| if os.Getenv("DATABASE_URL") == "" { |
| os.Setenv("DATABASE_URL", "postgres://localhost/terraform_backend_pg_test?sslmode=disable") |
| } |
| } |
| |
| func TestBackend_impl(t *testing.T) { |
| var _ backend.Backend = new(Backend) |
| } |
| |
| func TestBackendConfig(t *testing.T) { |
| testACC(t) |
| connStr := getDatabaseUrl() |
| schemaName := pq.QuoteIdentifier(fmt.Sprintf("terraform_%s", t.Name())) |
| |
| config := backend.TestWrapConfig(map[string]interface{}{ |
| "conn_str": connStr, |
| "schema_name": schemaName, |
| }) |
| schemaName = pq.QuoteIdentifier(schemaName) |
| |
| dbCleaner, err := sql.Open("postgres", connStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) |
| |
| b := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if b == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| |
| _, err = b.db.Query(fmt.Sprintf("SELECT name, data FROM %s.%s LIMIT 1", schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = b.StateMgr(backend.DefaultStateName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| s, err := b.StateMgr(backend.DefaultStateName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| c := s.(*remote.State).Client.(*RemoteClient) |
| if c.Name != backend.DefaultStateName { |
| t.Fatal("RemoteClient name is not configured") |
| } |
| |
| backend.TestBackendStates(t, b) |
| } |
| |
| func TestBackendConfigSkipOptions(t *testing.T) { |
| testACC(t) |
| connStr := getDatabaseUrl() |
| |
| testCases := []struct { |
| Name string |
| SkipSchemaCreation bool |
| SkipTableCreation bool |
| SkipIndexCreation bool |
| TestIndexIsPresent bool |
| Setup func(t *testing.T, db *sql.DB, schemaName string) |
| }{ |
| { |
| Name: "skip_schema_creation", |
| SkipSchemaCreation: true, |
| TestIndexIsPresent: true, |
| Setup: func(t *testing.T, db *sql.DB, schemaName string) { |
| // create the schema as a prerequisites |
| _, err := db.Query(fmt.Sprintf(`CREATE SCHEMA IF NOT EXISTS %s`, schemaName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| }, |
| }, |
| { |
| Name: "skip_table_creation", |
| SkipTableCreation: true, |
| TestIndexIsPresent: true, |
| Setup: func(t *testing.T, db *sql.DB, schemaName string) { |
| // since the table needs to be already created the schema must be too |
| _, err := db.Query(fmt.Sprintf(`CREATE SCHEMA %s`, schemaName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, err = db.Query(fmt.Sprintf(`CREATE TABLE %s.%s ( |
| id SERIAL PRIMARY KEY, |
| name TEXT, |
| data TEXT |
| )`, schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| }, |
| }, |
| { |
| Name: "skip_index_creation", |
| SkipIndexCreation: true, |
| TestIndexIsPresent: true, |
| Setup: func(t *testing.T, db *sql.DB, schemaName string) { |
| // Everything need to exists for the index to be created |
| _, err := db.Query(fmt.Sprintf(`CREATE SCHEMA %s`, schemaName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, err = db.Query(fmt.Sprintf(`CREATE TABLE %s.%s ( |
| id SERIAL PRIMARY KEY, |
| name TEXT, |
| data TEXT |
| )`, schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, err = db.Exec(fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s.%s (name)`, statesIndexName, schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| }, |
| }, |
| { |
| Name: "missing_index", |
| SkipIndexCreation: true, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.Name, func(t *testing.T) { |
| schemaName := tc.Name |
| |
| config := backend.TestWrapConfig(map[string]interface{}{ |
| "conn_str": connStr, |
| "schema_name": schemaName, |
| "skip_schema_creation": tc.SkipSchemaCreation, |
| "skip_table_creation": tc.SkipTableCreation, |
| "skip_index_creation": tc.SkipIndexCreation, |
| }) |
| schemaName = pq.QuoteIdentifier(schemaName) |
| db, err := sql.Open("postgres", connStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if tc.Setup != nil { |
| tc.Setup(t, db, schemaName) |
| } |
| defer db.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) |
| |
| b := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if b == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| |
| // Make sure everything has been created |
| |
| // This tests that both the schema and the table have been created |
| _, err = b.db.Query(fmt.Sprintf("SELECT name, data FROM %s.%s LIMIT 1", schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if tc.TestIndexIsPresent { |
| // Make sure that the index exists |
| query := `select count(*) from pg_indexes where schemaname=$1 and tablename=$2 and indexname=$3;` |
| var count int |
| if err := b.db.QueryRow(query, tc.Name, statesTableName, statesIndexName).Scan(&count); err != nil { |
| t.Fatal(err) |
| } |
| if count != 1 { |
| t.Fatalf("The index has not been created (%d)", count) |
| } |
| } |
| |
| _, err = b.StateMgr(backend.DefaultStateName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| s, err := b.StateMgr(backend.DefaultStateName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| c := s.(*remote.State).Client.(*RemoteClient) |
| if c.Name != backend.DefaultStateName { |
| t.Fatal("RemoteClient name is not configured") |
| } |
| |
| // Make sure that all workspace must have a unique name |
| _, err = db.Exec(fmt.Sprintf(`INSERT INTO %s.%s VALUES (100, 'unique_name_test', '')`, schemaName, statesTableName)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, err = db.Exec(fmt.Sprintf(`INSERT INTO %s.%s VALUES (101, 'unique_name_test', '')`, schemaName, statesTableName)) |
| if err == nil { |
| t.Fatal("Creating two workspaces with the same name did not raise an error") |
| } |
| }) |
| } |
| |
| } |
| |
| func TestBackendStates(t *testing.T) { |
| testACC(t) |
| connStr := getDatabaseUrl() |
| |
| testCases := []string{ |
| fmt.Sprintf("terraform_%s", t.Name()), |
| fmt.Sprintf("test with spaces: %s", t.Name()), |
| } |
| for _, schemaName := range testCases { |
| t.Run(schemaName, func(t *testing.T) { |
| dbCleaner, err := sql.Open("postgres", connStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer dbCleaner.Query("DROP SCHEMA IF EXISTS %s CASCADE", pq.QuoteIdentifier(schemaName)) |
| |
| config := backend.TestWrapConfig(map[string]interface{}{ |
| "conn_str": connStr, |
| "schema_name": schemaName, |
| }) |
| b := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if b == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| |
| backend.TestBackendStates(t, b) |
| }) |
| } |
| } |
| |
| func TestBackendStateLocks(t *testing.T) { |
| testACC(t) |
| connStr := getDatabaseUrl() |
| schemaName := fmt.Sprintf("terraform_%s", t.Name()) |
| dbCleaner, err := sql.Open("postgres", connStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) |
| |
| config := backend.TestWrapConfig(map[string]interface{}{ |
| "conn_str": connStr, |
| "schema_name": schemaName, |
| }) |
| b := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if b == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| |
| bb := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if bb == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| |
| backend.TestBackendStateLocks(t, b, bb) |
| } |
| |
| func TestBackendConcurrentLock(t *testing.T) { |
| testACC(t) |
| connStr := getDatabaseUrl() |
| dbCleaner, err := sql.Open("postgres", connStr) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| getStateMgr := func(schemaName string) (statemgr.Full, *statemgr.LockInfo) { |
| defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) |
| config := backend.TestWrapConfig(map[string]interface{}{ |
| "conn_str": connStr, |
| "schema_name": schemaName, |
| }) |
| b := backend.TestBackendConfig(t, New(), config).(*Backend) |
| |
| if b == nil { |
| t.Fatal("Backend could not be configured") |
| } |
| stateMgr, err := b.StateMgr(backend.DefaultStateName) |
| if err != nil { |
| t.Fatalf("Failed to get the state manager: %v", err) |
| } |
| |
| info := statemgr.NewLockInfo() |
| info.Operation = "test" |
| info.Who = schemaName |
| |
| return stateMgr, info |
| } |
| |
| s1, i1 := getStateMgr(fmt.Sprintf("terraform_%s_1", t.Name())) |
| s2, i2 := getStateMgr(fmt.Sprintf("terraform_%s_2", t.Name())) |
| |
| // First we need to create the workspace as the lock for creating them is |
| // global |
| lockID1, err := s1.Lock(i1) |
| if err != nil { |
| t.Fatalf("failed to lock first state: %v", err) |
| } |
| |
| if err = s1.PersistState(); err != nil { |
| t.Fatalf("failed to persist state: %v", err) |
| } |
| |
| if err := s1.Unlock(lockID1); err != nil { |
| t.Fatalf("failed to unlock first state: %v", err) |
| } |
| |
| lockID2, err := s2.Lock(i2) |
| if err != nil { |
| t.Fatalf("failed to lock second state: %v", err) |
| } |
| |
| if err = s2.PersistState(); err != nil { |
| t.Fatalf("failed to persist state: %v", err) |
| } |
| |
| if err := s2.Unlock(lockID2); err != nil { |
| t.Fatalf("failed to unlock first state: %v", err) |
| } |
| |
| // Now we can test concurrent lock |
| lockID1, err = s1.Lock(i1) |
| if err != nil { |
| t.Fatalf("failed to lock first state: %v", err) |
| } |
| |
| lockID2, err = s2.Lock(i2) |
| if err != nil { |
| t.Fatalf("failed to lock second state: %v", err) |
| } |
| |
| if err := s1.Unlock(lockID1); err != nil { |
| t.Fatalf("failed to unlock first state: %v", err) |
| } |
| |
| if err := s2.Unlock(lockID2); err != nil { |
| t.Fatalf("failed to unlock first state: %v", err) |
| } |
| } |
| |
| func getDatabaseUrl() string { |
| return os.Getenv("DATABASE_URL") |
| } |