Skip to content

Commit 2b20a15

Browse files
authored
Merge pull request #76 from lukaszbudnik/dev-v4.1
migrator v4.1
2 parents d8bec0f + 78485ff commit 2b20a15

File tree

10 files changed

+312
-45
lines changed

10 files changed

+312
-45
lines changed

README.md

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,17 @@ Further, there is an official docker image available on docker hub. [lukasz/migr
2626
* [4. Run migrator from official docker image](#4-run-migrator-from-official-docker-image)
2727
* [5. Play around with migrator](#5-play-around-with-migrator)
2828
* [Configuration](#configuration)
29+
* [migrator.yaml](#migratoryaml)
30+
* [Env variables substitution](#env-variables-substitution)
31+
* [Source migrations](#source-migrations)
32+
* [Local storage](#local-storage)
33+
* [AWS S3](#aws-s3)
34+
* [Supported databases](#supported-databases)
2935
* [Customisation and legacy frameworks support](#customisation-and-legacy-frameworks-support)
3036
* [Custom tenants support](#custom-tenants-support)
3137
* [Custom schema placeholder](#custom-schema-placeholder)
3238
* [Synchonising legacy migrations to migrator](#synchonising-legacy-migrations-to-migrator)
3339
* [Final comments](#final-comments)
34-
* [Supported databases](#supported-databases)
3540
* [Performance](#performance)
3641
* [Change log](#change-log)
3742
* [Contributing, code style, running unit & integration tests](#contributing-code-style-running-unit--integration-tests)
@@ -41,7 +46,7 @@ Further, there is an official docker image available on docker hub. [lukasz/migr
4146

4247
migrator exposes a simple REST API described below.
4348

44-
# GET /
49+
## GET /
4550

4651
Migrator returns build information together with supported API versions.
4752

@@ -464,7 +469,11 @@ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "new_tenant", "
464469

465470
# Configuration
466471

467-
migrator requires a simple `migrator.yaml` file:
472+
Let's see how to configure migrator.
473+
474+
## migrator.yaml
475+
476+
migrator configuration file is a simple YAML file. Take a look at a sample `migrator.yaml` configuration file which contains the description, correct syntax, and sample values for all available properties.
468477

469478
```yaml
470479
# required, base directory where all migrations are stored, see singleSchemas and tenantSchemas below
@@ -506,6 +515,8 @@ webHookHeaders:
506515
- "X-CustomHeader: value1,value2"
507516
```
508517
518+
## Env variables substitution
519+
509520
migrator supports env variables substitution in config file. All patterns matching `${NAME}` will look for env variable `NAME`. Below are some common use cases:
510521

511522
```yaml
@@ -514,6 +525,51 @@ webHookHeaders:
514525
- "X-Security-Token: ${SECURITY_TOKEN}"
515526
```
516527

528+
## Source migrations
529+
530+
Migrations can be read either from local disk or from S3 (I'm open to contributions to add more cloud storage options).
531+
532+
### Local storage
533+
534+
If `baseDir` property is a path (either relative or absolute) local storage implementation is used:
535+
536+
```
537+
# relative path
538+
baseDir: test/migrations
539+
# absolute path
540+
baseDir: /project/migrations
541+
```
542+
543+
### AWS S3
544+
545+
If `baseDir` starts with `s3://` prefix, AWS S3 implementation is used. In such case the `baseDir` property is treated as a bucket name:
546+
547+
```
548+
# S3 bucket
549+
baseDir: s3://lukasz-budnik-migrator-us-east-1
550+
```
551+
552+
migrator uses official AWS SDK for Go and uses a well known [default credential provider chain](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html). Please setup your env variables accordingly.
553+
554+
## Supported databases
555+
556+
Currently migrator supports the following databases and their flavours. Please review the Go driver implementation for information about supported features and how `dataSource` configuration property should look like:
557+
558+
* PostgreSQL 9.3+ - schema-based multi-tenant database, with transactions spanning DDL statements, driver used: https://github.com/lib/pq
559+
* PostgreSQL
560+
* Amazon RDS PostgreSQL - PostgreSQL-compatible relational database built for the cloud
561+
* Amazon Aurora PostgreSQL - PostgreSQL-compatible relational database built for the cloud
562+
* Google CloudSQL PostgreSQL - PostgreSQL-compatible relational database built for the cloud
563+
* MySQL 5.6+ - database-based multi-tenant database, transactions do not span DDL statements, driver used: https://github.com/go-sql-driver/mysql
564+
* MySQL
565+
* MariaDB - enhanced near linearly scalable multi-master MySQL
566+
* Percona - an enhanced drop-in replacement for MySQL
567+
* Amazon RDS MySQL - MySQL-compatible relational database built for the cloud
568+
* Amazon Aurora MySQL - MySQL-compatible relational database built for the cloud
569+
* Google CloudSQL MySQL - MySQL-compatible relational database built for the cloud
570+
* Microsoft SQL Server 2017 - a relational database management system developed by Microsoft, driver used: https://github.com/denisenkom/go-mssqldb
571+
* Microsoft SQL Server
572+
517573
# Customisation and legacy frameworks support
518574

519575
migrator can be used with an already existing legacy DB migration framework.
@@ -575,25 +631,6 @@ When using migrator please remember that:
575631
* when adding a new tenant migrator creates a new DB schema and applies all tenant migrations and scripts - no need to apply them manually
576632
* single schemas are not created automatically, you must add initial migration with `create schema {schema}` SQL statement (see examples above)
577633

578-
# Supported databases
579-
580-
Currently migrator supports the following databases and their flavours:
581-
582-
* PostgreSQL 9.3+ - schema-based multi-tenant database, with transactions spanning DDL statements, driver used: https://github.com/lib/pq
583-
* PostgreSQL - original PostgreSQL server
584-
* Amazon RDS PostgreSQL - PostgreSQL-compatible relational database built for the cloud
585-
* Amazon Aurora PostgreSQL - PostgreSQL-compatible relational database built for the cloud
586-
* Google CloudSQL PostgreSQL - PostgreSQL-compatible relational database built for the cloud
587-
* MySQL 5.6+ - database-based multi-tenant database, transactions do not span DDL statements, driver used: https://github.com/go-sql-driver/mysql
588-
* MySQL - original MySQL server
589-
* MariaDB - enhanced near linearly scalable multi-master MySQL
590-
* Percona - an enhanced drop-in replacement for MySQL
591-
* Amazon RDS MySQL - MySQL-compatible relational database built for the cloud
592-
* Amazon Aurora MySQL - MySQL-compatible relational database built for the cloud
593-
* Google CloudSQL MySQL - MySQL-compatible relational database built for the cloud
594-
* Microsoft SQL Server 2017 - a relational database management system developed by Microsoft, driver used: https://github.com/denisenkom/go-mssqldb
595-
* Microsoft SQL Server - original Microsoft SQL Server
596-
597634
# Performance
598635

599636
As a benchmarks I used 2 migrations frameworks:

loader/disk_loader.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
package loader
22

33
import (
4-
"context"
54
"crypto/sha256"
65
"encoding/hex"
76
"io/ioutil"
87
"path/filepath"
9-
"sort"
108
"strings"
119

1210
"fmt"
1311

14-
"github.com/lukaszbudnik/migrator/config"
1512
"github.com/lukaszbudnik/migrator/types"
1613
)
1714

1815
// diskLoader is struct used for implementing Loader interface for loading migrations from disk
1916
type diskLoader struct {
20-
ctx context.Context
21-
config *config.Config
17+
baseLoader
2218
}
2319

2420
// GetSourceMigrations returns all migrations from disk
@@ -36,24 +32,17 @@ func (dl *diskLoader) GetSourceMigrations() []types.Migration {
3632
tenantScriptsDirs := dl.getDirs(absBaseDir, dl.config.TenantScripts)
3733

3834
migrationsMap := make(map[string][]types.Migration)
39-
4035
dl.readFromDirs(migrationsMap, singleMigrationsDirs, types.MigrationTypeSingleMigration)
4136
dl.readFromDirs(migrationsMap, tenantMigrationsDirs, types.MigrationTypeTenantMigration)
42-
dl.readFromDirs(migrationsMap, singleScriptsDirs, types.MigrationTypeSingleScript)
43-
dl.readFromDirs(migrationsMap, tenantScriptsDirs, types.MigrationTypeTenantScript)
37+
dl.sortMigrations(migrationsMap, &migrations)
4438

45-
keys := make([]string, 0, len(migrationsMap))
46-
for key := range migrationsMap {
47-
keys = append(keys, key)
48-
}
49-
sort.Strings(keys)
39+
migrationsMap = make(map[string][]types.Migration)
40+
dl.readFromDirs(migrationsMap, singleScriptsDirs, types.MigrationTypeSingleScript)
41+
dl.sortMigrations(migrationsMap, &migrations)
5042

51-
for _, key := range keys {
52-
ms := migrationsMap[key]
53-
for _, m := range ms {
54-
migrations = append(migrations, m)
55-
}
56-
}
43+
migrationsMap = make(map[string][]types.Migration)
44+
dl.readFromDirs(migrationsMap, tenantScriptsDirs, types.MigrationTypeTenantScript)
45+
dl.sortMigrations(migrationsMap, &migrations)
5746

5847
return migrations
5948
}

loader/disk_loader_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func TestDiskGetDiskMigrations(t *testing.T) {
6767
loader := New(context.TODO(), &config)
6868
migrations := loader.GetSourceMigrations()
6969

70-
assert.Len(t, migrations, 10)
70+
assert.Len(t, migrations, 12)
7171

7272
assert.Contains(t, migrations[0].File, "test/migrations/config/201602160001.sql")
7373
assert.Contains(t, migrations[1].File, "test/migrations/config/201602160002.sql")
@@ -77,6 +77,10 @@ func TestDiskGetDiskMigrations(t *testing.T) {
7777
assert.Contains(t, migrations[5].File, "test/migrations/ref/201602160004.sql")
7878
assert.Contains(t, migrations[6].File, "test/migrations/tenants/201602160004.sql")
7979
assert.Contains(t, migrations[7].File, "test/migrations/tenants/201602160005.sql")
80-
assert.Contains(t, migrations[8].File, "test/migrations/config-scripts/201912181227.sql")
81-
assert.Contains(t, migrations[9].File, "test/migrations/tenants-scripts/201912181228.sql")
80+
// SingleScripts are second to last
81+
assert.Contains(t, migrations[8].File, "test/migrations/config-scripts/200012181227.sql")
82+
// TenantScripts are last
83+
assert.Contains(t, migrations[9].File, "test/migrations/tenants-scripts/200001181228.sql")
84+
assert.Contains(t, migrations[10].File, "test/migrations/tenants-scripts/a.sql")
85+
assert.Contains(t, migrations[11].File, "test/migrations/tenants-scripts/b.sql")
8286
}

loader/loader.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package loader
22

33
import (
44
"context"
5+
"sort"
6+
"strings"
57

68
"github.com/lukaszbudnik/migrator/config"
79
"github.com/lukaszbudnik/migrator/types"
@@ -17,5 +19,29 @@ type Factory func(context.Context, *config.Config) Loader
1719

1820
// New returns new instance of Loader, currently DiskLoader is available
1921
func New(ctx context.Context, config *config.Config) Loader {
20-
return &diskLoader{ctx, config}
22+
if strings.HasPrefix(config.BaseDir, "s3://") {
23+
return &s3Loader{baseLoader{ctx, config}}
24+
}
25+
return &diskLoader{baseLoader{ctx, config}}
26+
}
27+
28+
// baseLoader is the base struct for implementing Loader interface
29+
type baseLoader struct {
30+
ctx context.Context
31+
config *config.Config
32+
}
33+
34+
func (bl *baseLoader) sortMigrations(migrationsMap map[string][]types.Migration, migrations *[]types.Migration) {
35+
keys := make([]string, 0, len(migrationsMap))
36+
for key := range migrationsMap {
37+
keys = append(keys, key)
38+
}
39+
sort.Strings(keys)
40+
41+
for _, key := range keys {
42+
ms := migrationsMap[key]
43+
for _, m := range ms {
44+
*migrations = append(*migrations, m)
45+
}
46+
}
2147
}

loader/s3_loader.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package loader
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/aws/aws-sdk-go/aws"
11+
"github.com/aws/aws-sdk-go/aws/session"
12+
"github.com/aws/aws-sdk-go/service/s3"
13+
"github.com/aws/aws-sdk-go/service/s3/s3iface"
14+
"github.com/lukaszbudnik/migrator/types"
15+
)
16+
17+
// s3Loader is struct used for implementing Loader interface for loading migrations from AWS S3
18+
type s3Loader struct {
19+
baseLoader
20+
}
21+
22+
// GetSourceMigrations returns all migrations from AWS S3 location
23+
func (s3l *s3Loader) GetSourceMigrations() []types.Migration {
24+
sess, err := session.NewSession()
25+
if err != nil {
26+
panic(err.Error())
27+
}
28+
client := s3.New(sess)
29+
return s3l.doGetSourceMigrations(client)
30+
}
31+
32+
func (s3l *s3Loader) doGetSourceMigrations(client s3iface.S3API) []types.Migration {
33+
migrations := []types.Migration{}
34+
35+
singleMigrationsObjects := s3l.getObjectList(client, s3l.config.SingleMigrations)
36+
tenantMigrationsObjects := s3l.getObjectList(client, s3l.config.TenantMigrations)
37+
singleScriptsObjects := s3l.getObjectList(client, s3l.config.SingleScripts)
38+
tenantScriptsObjects := s3l.getObjectList(client, s3l.config.TenantScripts)
39+
40+
migrationsMap := make(map[string][]types.Migration)
41+
s3l.getObjects(client, migrationsMap, singleMigrationsObjects, types.MigrationTypeSingleMigration)
42+
s3l.getObjects(client, migrationsMap, tenantMigrationsObjects, types.MigrationTypeTenantMigration)
43+
s3l.sortMigrations(migrationsMap, &migrations)
44+
45+
migrationsMap = make(map[string][]types.Migration)
46+
s3l.getObjects(client, migrationsMap, singleScriptsObjects, types.MigrationTypeSingleScript)
47+
s3l.sortMigrations(migrationsMap, &migrations)
48+
49+
migrationsMap = make(map[string][]types.Migration)
50+
s3l.getObjects(client, migrationsMap, tenantScriptsObjects, types.MigrationTypeTenantScript)
51+
s3l.sortMigrations(migrationsMap, &migrations)
52+
53+
return migrations
54+
}
55+
56+
func (s3l *s3Loader) getObjectList(client s3iface.S3API, prefixes []string) []*string {
57+
objects := []*string{}
58+
59+
bucket := strings.Replace(s3l.config.BaseDir, "s3://", "", 1)
60+
61+
for _, prefix := range prefixes {
62+
63+
input := &s3.ListObjectsV2Input{
64+
Bucket: aws.String(bucket),
65+
Prefix: aws.String(prefix),
66+
MaxKeys: aws.Int64(1000),
67+
}
68+
69+
pageNum := 0
70+
err := client.ListObjectsV2Pages(input,
71+
func(page *s3.ListObjectsV2Output, lastPage bool) bool {
72+
pageNum++
73+
for _, o := range page.Contents {
74+
objects = append(objects, o.Key)
75+
}
76+
77+
return pageNum <= 10
78+
})
79+
80+
if err != nil {
81+
panic(err.Error())
82+
}
83+
}
84+
85+
return objects
86+
}
87+
88+
func (s3l *s3Loader) getObjects(client s3iface.S3API, migrationsMap map[string][]types.Migration, objects []*string, migrationType types.MigrationType) {
89+
bucket := strings.Replace(s3l.config.BaseDir, "s3://", "", 1)
90+
91+
objectInput := &s3.GetObjectInput{Bucket: aws.String(bucket)}
92+
for _, o := range objects {
93+
objectInput.Key = o
94+
objectOutput, err := client.GetObject(objectInput)
95+
if err != nil {
96+
panic(err.Error())
97+
}
98+
buf := new(bytes.Buffer)
99+
buf.ReadFrom(objectOutput.Body)
100+
contents := buf.String()
101+
102+
hasher := sha256.New()
103+
hasher.Write([]byte(contents))
104+
file := fmt.Sprintf("%s/%s", s3l.config.BaseDir, *o)
105+
from := strings.LastIndex(file, "/")
106+
sourceDir := file[0:from]
107+
name := file[from+1:]
108+
m := types.Migration{Name: name, SourceDir: sourceDir, File: file, MigrationType: migrationType, Contents: string(contents), CheckSum: hex.EncodeToString(hasher.Sum(nil))}
109+
110+
e, ok := migrationsMap[m.Name]
111+
if ok {
112+
e = append(e, m)
113+
} else {
114+
e = []types.Migration{m}
115+
}
116+
migrationsMap[m.Name] = e
117+
118+
}
119+
}

0 commit comments

Comments
 (0)