mirror of
https://github.com/Icinga/icingadb.git
synced 2026-05-28 04:35:54 -04:00
Auto Import Database Schema
To get rid of docker-icingadb and its additional entry point, the schema import functionality has been implemented directly in Icinga DB. Using the new --database-auto-import command line argument will result in an automatic schema import if no schema is found. The implementation is split between the already existing CheckSchema function and the introduced ImportSchema function. The CheckSchema function is now able to distinguish between the absence of a schema and an incorrect schema version. Both situations return a separate error type. As before, CheckSchema is called in the main function. If the error type now implies the absence of a schema (ErrSchemaNotExists) and the --database-auto-import flag is set, the auto-import is started. The schema import itself is performed in the new ImportSchema function, which loads the schema from a given file and inserts it within a transaction, allowing to rollback in case of an error. Fixes #896.
This commit is contained in:
parent
eb0b947e3f
commit
48d4305e92
3 changed files with 91 additions and 11 deletions
|
|
@ -67,7 +67,18 @@ func run() int {
|
|||
}
|
||||
}
|
||||
|
||||
if err := icingadb.CheckSchema(context.Background(), db); err != nil {
|
||||
switch err := icingadb.CheckSchema(context.Background(), db); {
|
||||
case errors.Is(err, icingadb.ErrSchemaNotExists):
|
||||
if !cmd.Flags.DatabaseAutoImport {
|
||||
logger.Fatal("The database schema is missing")
|
||||
}
|
||||
|
||||
logger.Info("Starting database schema auto import")
|
||||
if err := icingadb.ImportSchema(context.Background(), db, cmd.Flags.DatabaseSchemaDir); err != nil {
|
||||
logger.Fatalf("%+v", errors.Wrap(err, "can't import database schema"))
|
||||
}
|
||||
logger.Info("The database schema was successfully imported")
|
||||
case err != nil:
|
||||
logger.Fatalf("%+v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ type Flags struct {
|
|||
// Config is the path to the config file. If not provided, it defaults to DefaultConfigPath.
|
||||
Config string `short:"c" long:"config" description:"path to config file (default: /etc/icingadb/config.yml)"`
|
||||
// default must be kept in sync with DefaultConfigPath.
|
||||
|
||||
// DatabaseAutoImport results in an initial schema check and update; mostly for containerized setups.
|
||||
DatabaseAutoImport bool `long:"database-auto-import" description:"import database schema on startup if database is empty"`
|
||||
|
||||
// DatabaseSchemaDir is the root directory for schema files to be used when DatabaseAutoImport is requested.
|
||||
//
|
||||
// The directory structure must mimic the git repo's schema dir, containing ./mysql/schema.sql and ./pgsql/schema.sql.
|
||||
DatabaseSchemaDir string `long:"database-schema-dir" description:"directory for --database-auto-import, expects ./{my,pg}sql/schema.sql files" default:"./schema/"`
|
||||
}
|
||||
|
||||
// GetConfigPath retrieves the path to the configuration file.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ package icingadb
|
|||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"github.com/icinga/icinga-go-library/backoff"
|
||||
"github.com/icinga/icinga-go-library/database"
|
||||
"github.com/icinga/icinga-go-library/retry"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -15,7 +19,19 @@ const (
|
|||
expectedPostgresSchemaVersion = 4
|
||||
)
|
||||
|
||||
// CheckSchema asserts the database schema of the expected version being present.
|
||||
// ErrSchemaNotExists implies that no Icinga DB schema has been imported.
|
||||
var ErrSchemaNotExists = stderrors.New("no database schema exists")
|
||||
|
||||
// ErrSchemaMismatch implies an unexpected schema version, most likely after Icinga DB was updated but the database
|
||||
// missed the schema upgrade.
|
||||
var ErrSchemaMismatch = stderrors.New("unexpected database schema version")
|
||||
|
||||
// CheckSchema verifies the correct database schema is present.
|
||||
//
|
||||
// This function returns the following error types, possibly wrapped:
|
||||
// - If no schema exists, the error returned is ErrSchemaNotExists.
|
||||
// - If the schema version does not match the expected version, the error returned is ErrSchemaMismatch.
|
||||
// - Otherwise, the original error is returned, for example in case of general database problems.
|
||||
func CheckSchema(ctx context.Context, db *database.DB) error {
|
||||
var expectedDbSchemaVersion uint16
|
||||
switch db.DriverName() {
|
||||
|
|
@ -23,19 +39,25 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
|
|||
expectedDbSchemaVersion = expectedMysqlSchemaVersion
|
||||
case database.PostgreSQL:
|
||||
expectedDbSchemaVersion = expectedPostgresSchemaVersion
|
||||
default:
|
||||
return errors.Errorf("unsupported database driver %q", db.DriverName())
|
||||
}
|
||||
|
||||
if hasSchemaTable, err := db.HasTable(ctx, "icingadb_schema"); err != nil {
|
||||
return errors.Wrap(err, "can't verify existence of database schema table")
|
||||
} else if !hasSchemaTable {
|
||||
return ErrSchemaNotExists
|
||||
}
|
||||
|
||||
var version uint16
|
||||
|
||||
err := retry.WithBackoff(
|
||||
ctx,
|
||||
func(ctx context.Context) (err error) {
|
||||
func(ctx context.Context) error {
|
||||
query := "SELECT version FROM icingadb_schema ORDER BY id DESC LIMIT 1"
|
||||
err = db.QueryRowxContext(ctx, query).Scan(&version)
|
||||
if err != nil {
|
||||
err = database.CantPerformQuery(err, query)
|
||||
if err := db.QueryRowxContext(ctx, query).Scan(&version); err != nil {
|
||||
return database.CantPerformQuery(err, query)
|
||||
}
|
||||
return
|
||||
return nil
|
||||
},
|
||||
retry.Retryable,
|
||||
backoff.NewExponentialWithJitter(128*time.Millisecond, 1*time.Minute),
|
||||
|
|
@ -48,11 +70,50 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
|
|||
// Since these error messages are trivial and mostly caused by users, we don't need
|
||||
// to print a stack trace here. However, since errors.Errorf() does this automatically,
|
||||
// we need to use fmt instead.
|
||||
return fmt.Errorf(
|
||||
"unexpected database schema version: v%d (expected v%d), please make sure you have applied all database"+
|
||||
" migrations after upgrading Icinga DB", version, expectedDbSchemaVersion,
|
||||
return fmt.Errorf("%w: v%d (expected v%d), please make sure you have applied all database"+
|
||||
" migrations after upgrading Icinga DB", ErrSchemaMismatch, version, expectedDbSchemaVersion,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportSchema performs an initial schema import in the db.
|
||||
//
|
||||
// This function assumes that no schema exists. So it should only be called after a prior CheckSchema call.
|
||||
func ImportSchema(
|
||||
ctx context.Context,
|
||||
db *database.DB,
|
||||
databaseSchemaDir string,
|
||||
) error {
|
||||
var schemaFileDirPart string
|
||||
switch db.DriverName() {
|
||||
case database.MySQL:
|
||||
schemaFileDirPart = "mysql"
|
||||
case database.PostgreSQL:
|
||||
schemaFileDirPart = "pgsql"
|
||||
default:
|
||||
return errors.Errorf("unsupported database driver %q", db.DriverName())
|
||||
}
|
||||
|
||||
schemaFile := path.Join(databaseSchemaDir, schemaFileDirPart, "schema.sql")
|
||||
schema, err := os.ReadFile(schemaFile) // #nosec G304 -- path is constructed from "trusted" command line user input
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "can't open schema file %q", schemaFile)
|
||||
}
|
||||
|
||||
queries := []string{string(schema)}
|
||||
if db.DriverName() == database.MySQL {
|
||||
// MySQL/MariaDB requires the schema to be imported on a statement by statement basis.
|
||||
queries = database.MysqlSplitStatements(string(schema))
|
||||
}
|
||||
|
||||
return errors.Wrapf(db.ExecTx(ctx, func(ctx context.Context, tx *sqlx.Tx) error {
|
||||
for _, query := range queries {
|
||||
if _, err := tx.ExecContext(ctx, query); err != nil {
|
||||
return errors.Wrap(database.CantPerformQuery(err, query), "can't perform schema import")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}), "can't import database schema from %q", schemaFile)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue