Skip to content

Commit 4be9d4d

Browse files
committed
Ditch dbmate in favour of internal migration
such that migration files can be embedded
1 parent 1b5f0dd commit 4be9d4d

File tree

7 files changed

+226
-44
lines changed

7 files changed

+226
-44
lines changed

backend/cmd/server/main.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,25 @@ func main() {
2222
config.Init(&version, &commit, &sentryDSN)
2323
appstate := state.NewState()
2424

25-
setting.ApplySettings()
26-
database.CheckSetup()
27-
28-
go worker.StartCertificateWorker(appstate)
29-
30-
api.StartServer()
31-
irqchan := make(chan os.Signal, 1)
32-
signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM)
33-
34-
for irq := range irqchan {
35-
if irq == syscall.SIGINT || irq == syscall.SIGTERM {
36-
logger.Info("Got ", irq, " shutting server down ...")
37-
// Close db
38-
err := database.GetInstance().Close()
39-
if err != nil {
40-
logger.Error("DatabaseCloseError", err)
25+
database.Migrate(func() {
26+
setting.ApplySettings()
27+
database.CheckSetup()
28+
go worker.StartCertificateWorker(appstate)
29+
30+
api.StartServer()
31+
irqchan := make(chan os.Signal, 1)
32+
signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM)
33+
34+
for irq := range irqchan {
35+
if irq == syscall.SIGINT || irq == syscall.SIGTERM {
36+
logger.Info("Got ", irq, " shutting server down ...")
37+
// Close db
38+
err := database.GetInstance().Close()
39+
if err != nil {
40+
logger.Error("DatabaseCloseError", err)
41+
}
42+
break
4143
}
42-
break
4344
}
44-
}
45+
})
4546
}

backend/internal/database/migrator.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package database
2+
3+
import (
4+
"database/sql"
5+
"embed"
6+
"fmt"
7+
"io/fs"
8+
"path"
9+
"path/filepath"
10+
"strings"
11+
"sync"
12+
"time"
13+
14+
"npm/internal/logger"
15+
"npm/internal/util"
16+
17+
"github.com/jmoiron/sqlx"
18+
)
19+
20+
//go:embed migrations/*.sql
21+
var migrationFiles embed.FS
22+
23+
// MigrationConfiguration options for the migrator.
24+
type MigrationConfiguration struct {
25+
Table string `json:"table"`
26+
mux sync.Mutex
27+
}
28+
29+
// Default migrator configuration
30+
var mConfiguration = MigrationConfiguration{
31+
Table: "migration",
32+
}
33+
34+
// ConfigureMigrator and will return error if missing required fields.
35+
func ConfigureMigrator(c *MigrationConfiguration) error {
36+
// ensure updates to the config are atomic
37+
mConfiguration.mux.Lock()
38+
defer mConfiguration.mux.Unlock()
39+
if c == nil {
40+
return fmt.Errorf("a non nil Configuration is mandatory")
41+
}
42+
if strings.TrimSpace(c.Table) != "" {
43+
mConfiguration.Table = c.Table
44+
}
45+
mConfiguration.Table = c.Table
46+
return nil
47+
}
48+
49+
type afterMigrationComplete func()
50+
51+
// Migrate will perform the migration from start to finish
52+
func Migrate(followup afterMigrationComplete) bool {
53+
logger.Info("Migration: Started")
54+
55+
// Try to connect to the database sleeping for 15 seconds in between
56+
var db *sqlx.DB
57+
for {
58+
db = GetInstance()
59+
if db == nil {
60+
logger.Warn("Database is unavailable for migration, retrying in 15 seconds")
61+
time.Sleep(15 * time.Second)
62+
} else {
63+
break
64+
}
65+
}
66+
67+
// Check for migration table existence
68+
if !tableExists(db, mConfiguration.Table) {
69+
err := createMigrationTable(db)
70+
if err != nil {
71+
logger.Error("MigratorError", err)
72+
return false
73+
}
74+
logger.Info("Migration: Migration Table created")
75+
}
76+
77+
// DO MIGRATION
78+
migrationCount, migrateErr := performFileMigrations(db)
79+
if migrateErr != nil {
80+
logger.Error("MigratorError", migrateErr)
81+
}
82+
83+
if migrateErr == nil {
84+
logger.Info("Migration: Completed %v migration files", migrationCount)
85+
followup()
86+
return true
87+
}
88+
return false
89+
}
90+
91+
// createMigrationTable performs a query to create the migration table
92+
// with the name specified in the configuration
93+
func createMigrationTable(db *sqlx.DB) error {
94+
logger.Info("Migration: Creating Migration Table: %v", mConfiguration.Table)
95+
// nolint:lll
96+
query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%v` (filename TEXT PRIMARY KEY, migrated_on INTEGER NOT NULL DEFAULT 0)", mConfiguration.Table)
97+
_, err := db.Exec(query)
98+
return err
99+
}
100+
101+
// tableExists will check the database for the existence of the specified table.
102+
func tableExists(db *sqlx.DB, tableName string) bool {
103+
query := `SELECT name FROM sqlite_master WHERE type='table' AND name = $1`
104+
105+
row := db.QueryRowx(query, tableName)
106+
if row == nil {
107+
logger.Error("MigratorError", fmt.Errorf("Cannot check if table exists, no row returned: %v", tableName))
108+
return false
109+
}
110+
111+
var exists *bool
112+
if err := row.Scan(&exists); err != nil {
113+
if err == sql.ErrNoRows {
114+
return false
115+
}
116+
logger.Error("MigratorError", err)
117+
return false
118+
}
119+
return *exists
120+
}
121+
122+
// performFileMigrations will perform the actual migration,
123+
// importing files and updating the database with the rows imported.
124+
func performFileMigrations(db *sqlx.DB) (int, error) {
125+
var importedCount = 0
126+
127+
// Grab a list of previously ran migrations from the database:
128+
previousMigrations, prevErr := getPreviousMigrations(db)
129+
if prevErr != nil {
130+
return importedCount, prevErr
131+
}
132+
133+
// List up the ".sql" files on disk
134+
err := fs.WalkDir(migrationFiles, ".", func(file string, d fs.DirEntry, err error) error {
135+
if !d.IsDir() {
136+
shortFile := filepath.Base(file)
137+
138+
// Check if this file already exists in the previous migrations
139+
// and if so, ignore it
140+
if util.SliceContainsItem(previousMigrations, shortFile) {
141+
return nil
142+
}
143+
144+
logger.Info("Migration: Importing %v", shortFile)
145+
146+
sqlContents, ioErr := migrationFiles.ReadFile(path.Clean(file))
147+
if ioErr != nil {
148+
return ioErr
149+
}
150+
151+
sqlString := string(sqlContents)
152+
153+
tx := db.MustBegin()
154+
if _, execErr := tx.Exec(sqlString); execErr != nil {
155+
return execErr
156+
}
157+
if commitErr := tx.Commit(); commitErr != nil {
158+
return commitErr
159+
}
160+
if markErr := markMigrationSuccessful(db, shortFile); markErr != nil {
161+
return markErr
162+
}
163+
164+
importedCount++
165+
}
166+
return nil
167+
})
168+
169+
return importedCount, err
170+
}
171+
172+
// getPreviousMigrations will query the migration table for names
173+
// of migrations we can ignore because they should have already
174+
// been imported
175+
func getPreviousMigrations(db *sqlx.DB) ([]string, error) {
176+
var existingMigrations []string
177+
// nolint:gosec
178+
query := fmt.Sprintf("SELECT filename FROM `%v` ORDER BY filename", mConfiguration.Table)
179+
rows, err := db.Queryx(query)
180+
if err != nil {
181+
if err == sql.ErrNoRows {
182+
return existingMigrations, nil
183+
}
184+
return existingMigrations, err
185+
}
186+
187+
for rows.Next() {
188+
var filename *string
189+
err := rows.Scan(&filename)
190+
if err != nil {
191+
return existingMigrations, err
192+
}
193+
existingMigrations = append(existingMigrations, *filename)
194+
}
195+
196+
return existingMigrations, nil
197+
}
198+
199+
// markMigrationSuccessful will add a row to the migration table
200+
func markMigrationSuccessful(db *sqlx.DB, filename string) error {
201+
// nolint:gosec
202+
query := fmt.Sprintf("INSERT INTO `%v` (filename) VALUES ($1)", mConfiguration.Table)
203+
_, err := db.Exec(query, filename)
204+
return err
205+
}

docker/Dockerfile

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ RUN mkdir -p /dist \
3838
FROM jc21/nginx-full:github-acme.sh AS final
3939

4040
COPY --from=gobuild /dist/server /app/bin/server
41-
COPY backend/migrations /app/migrations
4241

4342
ENV SUPPRESS_NO_CONFIG_WARNING=1
4443
ENV S6_FIX_ATTRS_HIDDEN=1
@@ -66,11 +65,7 @@ ARG BUILD_VERSION
6665
ARG BUILD_COMMIT
6766
ARG BUILD_DATE
6867

69-
ENV DATABASE_URL="sqlite:////data/nginxproxymanager.db" \
70-
DBMATE_MIGRATIONS_DIR="/app/migrations" \
71-
DBMATE_NO_DUMP_SCHEMA="1" \
72-
DBMATE_SCHEMA_FILE="/data/schema.sql" \
73-
NPM_BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \
68+
ENV NPM_BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \
7469
NPM_BUILD_COMMIT="${BUILD_COMMIT:-dev}" \
7570
NPM_BUILD_DATE="${BUILD_DATE:-}"
7671

docker/dev/Dockerfile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ ENV GOPROXY=$GOPROXY \
1010
GOPRIVATE=$GOPRIVATE \
1111
S6_LOGGING=0 \
1212
SUPPRESS_NO_CONFIG_WARNING=1 \
13-
S6_FIX_ATTRS_HIDDEN=1 \
14-
DATABASE_URL="sqlite:////data/nginxproxymanager.db" \
15-
DBMATE_MIGRATIONS_DIR="/app/backend/migrations" \
16-
DBMATE_SCHEMA_FILE="/data/schema.sql"
13+
S6_FIX_ATTRS_HIDDEN=1
1714

1815
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
1916

docker/rootfs/etc/cont-init.d/30-dbmate

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)