Skip to content

Commit 6cbab3f

Browse files
authored
Merge pull request #901 from Icinga/auto-schema-import
Auto Import Database Schema
2 parents eb0b947 + 48d4305 commit 6cbab3f

File tree

3 files changed

+91
-11
lines changed

3 files changed

+91
-11
lines changed

cmd/icingadb/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,18 @@ func run() int {
6767
}
6868
}
6969

70-
if err := icingadb.CheckSchema(context.Background(), db); err != nil {
70+
switch err := icingadb.CheckSchema(context.Background(), db); {
71+
case errors.Is(err, icingadb.ErrSchemaNotExists):
72+
if !cmd.Flags.DatabaseAutoImport {
73+
logger.Fatal("The database schema is missing")
74+
}
75+
76+
logger.Info("Starting database schema auto import")
77+
if err := icingadb.ImportSchema(context.Background(), db, cmd.Flags.DatabaseSchemaDir); err != nil {
78+
logger.Fatalf("%+v", errors.Wrap(err, "can't import database schema"))
79+
}
80+
logger.Info("The database schema was successfully imported")
81+
case err != nil:
7182
logger.Fatalf("%+v", err)
7283
}
7384

internal/config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ type Flags struct {
5858
// Config is the path to the config file. If not provided, it defaults to DefaultConfigPath.
5959
Config string `short:"c" long:"config" description:"path to config file (default: /etc/icingadb/config.yml)"`
6060
// default must be kept in sync with DefaultConfigPath.
61+
62+
// DatabaseAutoImport results in an initial schema check and update; mostly for containerized setups.
63+
DatabaseAutoImport bool `long:"database-auto-import" description:"import database schema on startup if database is empty"`
64+
65+
// DatabaseSchemaDir is the root directory for schema files to be used when DatabaseAutoImport is requested.
66+
//
67+
// The directory structure must mimic the git repo's schema dir, containing ./mysql/schema.sql and ./pgsql/schema.sql.
68+
DatabaseSchemaDir string `long:"database-schema-dir" description:"directory for --database-auto-import, expects ./{my,pg}sql/schema.sql files" default:"./schema/"`
6169
}
6270

6371
// GetConfigPath retrieves the path to the configuration file.

pkg/icingadb/schema.go

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package icingadb
22

33
import (
44
"context"
5+
stderrors "errors"
56
"fmt"
67
"github.com/icinga/icinga-go-library/backoff"
78
"github.com/icinga/icinga-go-library/database"
89
"github.com/icinga/icinga-go-library/retry"
10+
"github.com/jmoiron/sqlx"
911
"github.com/pkg/errors"
12+
"os"
13+
"path"
1014
"time"
1115
)
1216

@@ -15,27 +19,45 @@ const (
1519
expectedPostgresSchemaVersion = 4
1620
)
1721

18-
// CheckSchema asserts the database schema of the expected version being present.
22+
// ErrSchemaNotExists implies that no Icinga DB schema has been imported.
23+
var ErrSchemaNotExists = stderrors.New("no database schema exists")
24+
25+
// ErrSchemaMismatch implies an unexpected schema version, most likely after Icinga DB was updated but the database
26+
// missed the schema upgrade.
27+
var ErrSchemaMismatch = stderrors.New("unexpected database schema version")
28+
29+
// CheckSchema verifies the correct database schema is present.
30+
//
31+
// This function returns the following error types, possibly wrapped:
32+
// - If no schema exists, the error returned is ErrSchemaNotExists.
33+
// - If the schema version does not match the expected version, the error returned is ErrSchemaMismatch.
34+
// - Otherwise, the original error is returned, for example in case of general database problems.
1935
func CheckSchema(ctx context.Context, db *database.DB) error {
2036
var expectedDbSchemaVersion uint16
2137
switch db.DriverName() {
2238
case database.MySQL:
2339
expectedDbSchemaVersion = expectedMysqlSchemaVersion
2440
case database.PostgreSQL:
2541
expectedDbSchemaVersion = expectedPostgresSchemaVersion
42+
default:
43+
return errors.Errorf("unsupported database driver %q", db.DriverName())
2644
}
2745

28-
var version uint16
46+
if hasSchemaTable, err := db.HasTable(ctx, "icingadb_schema"); err != nil {
47+
return errors.Wrap(err, "can't verify existence of database schema table")
48+
} else if !hasSchemaTable {
49+
return ErrSchemaNotExists
50+
}
2951

52+
var version uint16
3053
err := retry.WithBackoff(
3154
ctx,
32-
func(ctx context.Context) (err error) {
55+
func(ctx context.Context) error {
3356
query := "SELECT version FROM icingadb_schema ORDER BY id DESC LIMIT 1"
34-
err = db.QueryRowxContext(ctx, query).Scan(&version)
35-
if err != nil {
36-
err = database.CantPerformQuery(err, query)
57+
if err := db.QueryRowxContext(ctx, query).Scan(&version); err != nil {
58+
return database.CantPerformQuery(err, query)
3759
}
38-
return
60+
return nil
3961
},
4062
retry.Retryable,
4163
backoff.NewExponentialWithJitter(128*time.Millisecond, 1*time.Minute),
@@ -48,11 +70,50 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
4870
// Since these error messages are trivial and mostly caused by users, we don't need
4971
// to print a stack trace here. However, since errors.Errorf() does this automatically,
5072
// we need to use fmt instead.
51-
return fmt.Errorf(
52-
"unexpected database schema version: v%d (expected v%d), please make sure you have applied all database"+
53-
" migrations after upgrading Icinga DB", version, expectedDbSchemaVersion,
73+
return fmt.Errorf("%w: v%d (expected v%d), please make sure you have applied all database"+
74+
" migrations after upgrading Icinga DB", ErrSchemaMismatch, version, expectedDbSchemaVersion,
5475
)
5576
}
5677

5778
return nil
5879
}
80+
81+
// ImportSchema performs an initial schema import in the db.
82+
//
83+
// This function assumes that no schema exists. So it should only be called after a prior CheckSchema call.
84+
func ImportSchema(
85+
ctx context.Context,
86+
db *database.DB,
87+
databaseSchemaDir string,
88+
) error {
89+
var schemaFileDirPart string
90+
switch db.DriverName() {
91+
case database.MySQL:
92+
schemaFileDirPart = "mysql"
93+
case database.PostgreSQL:
94+
schemaFileDirPart = "pgsql"
95+
default:
96+
return errors.Errorf("unsupported database driver %q", db.DriverName())
97+
}
98+
99+
schemaFile := path.Join(databaseSchemaDir, schemaFileDirPart, "schema.sql")
100+
schema, err := os.ReadFile(schemaFile) // #nosec G304 -- path is constructed from "trusted" command line user input
101+
if err != nil {
102+
return errors.Wrapf(err, "can't open schema file %q", schemaFile)
103+
}
104+
105+
queries := []string{string(schema)}
106+
if db.DriverName() == database.MySQL {
107+
// MySQL/MariaDB requires the schema to be imported on a statement by statement basis.
108+
queries = database.MysqlSplitStatements(string(schema))
109+
}
110+
111+
return errors.Wrapf(db.ExecTx(ctx, func(ctx context.Context, tx *sqlx.Tx) error {
112+
for _, query := range queries {
113+
if _, err := tx.ExecContext(ctx, query); err != nil {
114+
return errors.Wrap(database.CantPerformQuery(err, query), "can't perform schema import")
115+
}
116+
}
117+
return nil
118+
}), "can't import database schema from %q", schemaFile)
119+
}

0 commit comments

Comments
 (0)