Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use shell environment variables in migrations using Go's text/template parsing #439

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
MYSQL_TEST_URL: mysql://root:root@mysql/dbmate_test
POSTGRES_TEST_URL: postgres://postgres:postgres@postgres/dbmate_test?sslmode=disable
SQLITE_TEST_URL: sqlite3:/tmp/dbmate_test.sqlite3
YABBA_DABBA_DOO: Yabba dabba doo!

dbmate:
build:
Expand Down
50 changes: 48 additions & 2 deletions pkg/dbmate/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dbmate

import (
"bytes"
"database/sql"
"errors"
"fmt"
Expand All @@ -11,6 +12,8 @@ import (
"path/filepath"
"regexp"
"sort"
"strings"
"text/template"
"time"

"github.com/amacneil/dbmate/v2/pkg/dbutil"
Expand Down Expand Up @@ -329,7 +332,12 @@ func (db *DB) Migrate() error {

execMigration := func(tx dbutil.Transaction) error {
// run actual migration
result, err := tx.Exec(parsed.Up)
upScript, err := db.resolveRefs(parsed.Up, parsed.UpOptions.EnvVars())
if err != nil {
return err
}

result, err := tx.Exec(upScript)
if err != nil {
return err
} else if db.Verbose {
Expand Down Expand Up @@ -361,6 +369,38 @@ func (db *DB) Migrate() error {
return nil
}

func (db *DB) resolveRefs(snippet string, envVars []string) (string, error) {
if envVars == nil {
return snippet, nil
}

envMap := db.getEnvMap()
model := make(map[string]string, len(envVars))
for _, envVar := range envVars {
model[envVar] = envMap[envVar]
}

template := template.Must(template.New("tmpl").Parse(snippet))

var buffer bytes.Buffer
if err := template.Execute(&buffer, model); err != nil {
return "", err
}

return buffer.String(), nil
}

func (db *DB) getEnvMap() map[string]string {
envMap := make(map[string]string)

for _, envVar := range os.Environ() {
entry := strings.SplitN(envVar, "=", 2)
envMap[entry[0]] = entry[1]
}

return envMap
}

func (db *DB) printVerbose(result sql.Result) {
lastInsertID, err := result.LastInsertId()
if err == nil {
Expand Down Expand Up @@ -491,7 +531,13 @@ func (db *DB) Rollback() error {

execMigration := func(tx dbutil.Transaction) error {
// rollback migration
result, err := tx.Exec(parsed.Down)
downScript, err := db.resolveRefs(parsed.Down, parsed.DownOptions.EnvVars())
if err != nil {
return err
}

result, err := tx.Exec(downScript)

if err != nil {
return err
} else if db.Verbose {
Expand Down
7 changes: 6 additions & 1 deletion pkg/dbmate/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func TestWaitBeforeVerbose(t *testing.T) {
`Applying: 20151129054053_test_migration.sql
Rows affected: 1
Applying: 20200227231541_test_posts.sql
Rows affected: 0`)
Rows affected: 1`)
require.Contains(t, output,
`Rolling back: 20200227231541_test_posts.sql
Rows affected: 0`)
Expand Down Expand Up @@ -315,6 +315,11 @@ func TestUp(t *testing.T) {
err = sqlDB.QueryRow("select count(*) from users").Scan(&count)
require.NoError(t, err)
require.Equal(t, 1, count)

var fromEnvVar string
err = sqlDB.QueryRow("select name from posts where id=1").Scan(&fromEnvVar)
require.NoError(t, err)
require.Equal(t, "Yabba dabba doo!", fromEnvVar)
})
}
}
Expand Down
29 changes: 28 additions & 1 deletion pkg/dbmate/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type ParsedMigration struct {
// ParsedMigrationOptions is an interface for accessing migration options
type ParsedMigrationOptions interface {
Transaction() bool
EnvVars() []string
}

type migrationOptions map[string]string
Expand All @@ -58,6 +59,21 @@ func (m migrationOptions) Transaction() bool {
return m["transaction"] != "false"
}

// EnvVars returns the list of env vars enabled to templating for this migration
// Defaults to empty list.
func (m migrationOptions) EnvVars() []string {
result := make([]string, 0)
entry := m["env"]

if entry != "" {
// decode CSV encoded var names
varNames := strings.Split(entry, ",")
// add to the slice
result = append(result, varNames...)
}
return result
}

var (
upRegExp = regexp.MustCompile(`(?m)^--\s*migrate:up(\s*$|\s+\S+)`)
downRegExp = regexp.MustCompile(`(?m)^--\s*migrate:down(\s*$|\s+\S+)$`)
Expand Down Expand Up @@ -143,7 +159,18 @@ func parseMigrationOptions(contents string) ParsedMigrationOptions {

// if the syntax is well-formed, then store the key and value pair in options
if len(pair) == 2 {
options[pair[0]] = pair[1]
optKey := pair[0]
optValue := pair[1]
entry := options[optKey]
if entry != "" { // "env" entry already used
varNames := strings.Split(entry, ",")
// add new element to the slice
varNames = append(varNames, optValue)
// keep collected values
options[optKey] = strings.Join(varNames, ",")
} else { // first "env" entry
options[optKey] = optValue
}
}
}

Expand Down
36 changes: 36 additions & 0 deletions pkg/dbmate/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ drop table users;

require.Equal(t, "--migrate:up\ncreate table users (id serial, name text);\n\n", parsed.Up)
require.Equal(t, true, parsed.UpOptions.Transaction())
require.Equal(t, []string{}, parsed.UpOptions.EnvVars())

require.Equal(t, "--migrate:down\ndrop table users;\n", parsed.Down)
require.Equal(t, true, parsed.DownOptions.Transaction())
require.Equal(t, []string{}, parsed.UpOptions.EnvVars())
})

t.Run("require up before down", func(t *testing.T) {
Expand Down Expand Up @@ -100,6 +102,40 @@ ALTER TYPE colors ADD VALUE 'orange' AFTER 'red';
require.Equal(t, false, parsed.DownOptions.Transaction())
})

t.Run("support activating env vars", func(t *testing.T) {
migration := `-- migrate:up env:THE_ROLE env:THE_PASSWORD
create role {{ .THE_ROLE }} login password {{ .THE_PASSWORD }};
-- migrate:down env:THE_ROLE
drop role {{ .THE_ROLE }};
`

parsed, err := parseMigrationContents(migration)
require.Nil(t, err)

require.Equal(t, "-- migrate:up env:THE_ROLE env:THE_PASSWORD\ncreate role {{ .THE_ROLE }} login password {{ .THE_PASSWORD }};\n", parsed.Up)
require.Equal(t, []string{"THE_ROLE", "THE_PASSWORD"}, parsed.UpOptions.EnvVars())

require.Equal(t, "-- migrate:down env:THE_ROLE\ndrop role {{ .THE_ROLE }};\n", parsed.Down)
require.Equal(t, []string{"THE_ROLE"}, parsed.DownOptions.EnvVars())
})

t.Run("support activating env vars", func(t *testing.T) {
migration := `-- migrate:up env:THE_ROLE env:THE_PASSWORD
create role {{ .THE_ROLE }} login password {{ .THE_PASSWORD }};
-- migrate:down env:THE_ROLE
drop role {{ .THE_ROLE }};
`

parsed, err := parseMigrationContents(migration)
require.Nil(t, err)

require.Equal(t, "-- migrate:up env:THE_ROLE env:THE_PASSWORD\ncreate role {{ .THE_ROLE }} login password {{ .THE_PASSWORD }};\n", parsed.Up)
require.Equal(t, []string{"THE_ROLE", "THE_PASSWORD"}, parsed.UpOptions.EnvVars())

require.Equal(t, "-- migrate:down env:THE_ROLE\ndrop role {{ .THE_ROLE }};\n", parsed.Down)
require.Equal(t, []string{"THE_ROLE"}, parsed.DownOptions.EnvVars())
})

t.Run("require migrate blocks", func(t *testing.T) {
migration := `
ALTER TABLE users
Expand Down
3 changes: 2 additions & 1 deletion testdata/db/migrations/20200227231541_test_posts.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
-- migrate:up
-- migrate:up env:YABBA_DABBA_DOO
create table posts (
id integer,
name varchar(255)
);
insert into posts (id, name) values (1, '{{ .YABBA_DABBA_DOO }}');

-- migrate:down
drop table posts;