Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 8 additions & 5 deletions cmd/icingadb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ import (
)

const (
ExitSuccess = 0
ExitFailure = 1
expectedRedisSchemaVersion = "6"
ExitSuccess = 0
ExitFailure = 1
)

func main() {
Expand Down Expand Up @@ -89,6 +88,10 @@ func run() int {
_ = db.Close()

logger.Info("The database schema was successfully imported")

case errors.Is(err, icingadb.ErrSchemaImperfect):
logger.Warnw("Database schema should be checked", zap.Error(err))

case err != nil:
logger.Fatalf("%+v", err)
}
Expand Down Expand Up @@ -421,13 +424,13 @@ func checkRedisSchema(logger *logging.Logger, rc *redis.Client, pos string) (new
}

message := streams[0].Messages[0]
if version := message.Values["version"]; version != expectedRedisSchemaVersion {
if version := message.Values["version"]; version != internal.RedisSchemaVersion {
// 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 Redis schema version: %q (expected %q), please make sure you are running compatible"+
" versions of Icinga 2 and Icinga DB", version, expectedRedisSchemaVersion,
" versions of Icinga 2 and Icinga DB", version, internal.RedisSchemaVersion,
)
}

Expand Down
30 changes: 30 additions & 0 deletions internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,33 @@ import (
//
// The placeholders are replaced on `git archive` using the `export-subst` attribute.
var Version = version.Version("1.5.1", "$Format:%(describe)$", "$Format:%H$")

// MySqlSchemaVersions maps MySQL/MariaDB schema versions to Icinga DB release version.
//
// Each schema version implies an available schema upgrade, named after the Icinga DB
// version and stored under ./schema/mysql/upgrades.
//
// The largest key implies the latest and expected schema version.
var MySqlSchemaVersions = map[uint16]string{
2: "1.0.0-rc2",
3: "1.0.0",
4: "1.1.1",
5: "1.2.0",
6: "1.2.1",
7: "1.4.0",
}

// PgSqlSchemaVersions maps PostgreSQL schema versions to Icinga DB release version.
//
// Same as MySqlSchemaVersions, but for PostgreSQL instead.
var PgSqlSchemaVersions = map[uint16]string{
2: "1.1.1",
3: "1.2.0",
4: "1.2.1",
5: "1.4.0",
}

// RedisSchemaVersion is the expected Redis schema version.
//
// This version must match between Icinga 2 and Icinga DB.
var RedisSchemaVersion = "6"
90 changes: 64 additions & 26 deletions pkg/icingadb/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import (
"context"
stderrors "errors"
"fmt"
"maps"
"os"
"path"
"slices"
"strings"

"github.com/icinga/icinga-go-library/backoff"
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/retry"
"github.com/icinga/icingadb/internal"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"os"
"path"
)

const (
expectedMysqlSchemaVersion = 7
expectedPostgresSchemaVersion = 5
)

// ErrSchemaNotExists implies that no Icinga DB schema has been imported.
Expand All @@ -25,23 +25,30 @@ var ErrSchemaNotExists = stderrors.New("no database schema exists")
// missed the schema upgrade.
var ErrSchemaMismatch = stderrors.New("unexpected database schema version")

// ErrSchemaImperfect implies some non critical failure condition of the database schema.
var ErrSchemaImperfect = stderrors.New("imperfect database schema")

// 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.
// - If there are non fatal database schema conditions, ErrSchemaImperfect is returned. This error must
// be reported back to the user, but should not lead in a program termination.
// - 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
var schemaVersions map[uint16]string
switch db.DriverName() {
case database.MySQL:
expectedDbSchemaVersion = expectedMysqlSchemaVersion
schemaVersions = internal.MySqlSchemaVersions
case database.PostgreSQL:
expectedDbSchemaVersion = expectedPostgresSchemaVersion
schemaVersions = internal.PgSqlSchemaVersions
default:
return errors.Errorf("unsupported database driver %q", db.DriverName())
}

expectedDbSchemaVersion := slices.Max(slices.Sorted(maps.Keys(schemaVersions)))

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 {
Expand All @@ -53,7 +60,7 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
err := retry.WithBackoff(
ctx,
func(ctx context.Context) error {
query := "SELECT version FROM icingadb_schema ORDER BY version ASC"
query := "SELECT version FROM icingadb_schema ORDER BY id ASC"
if err := db.SelectContext(ctx, &versions, query); err != nil {
return database.CantPerformQuery(err, query)
}
Expand All @@ -66,29 +73,60 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
return errors.Wrap(err, "can't check database schema version")
}

// In the following, multiple error conditions are checked.
//
// Since their 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.Errorf() instead.

// Check if any schema was imported.
if len(versions) == 0 {
return fmt.Errorf("%w: no database schema version is stored in the database", ErrSchemaMismatch)
}

// Check if each schema update between the initial import and the latest version was applied or, in other words,
// that no schema update was left out. The loop goes over the ascending sorted array of schema versions, verifying
// that each element's successor is the increment of this version, ensuring no gaps in between.
for i := 0; i < len(versions)-1; i++ {
if versions[i] != versions[i+1]-1 {
// Check if the latest schema version was imported.
if latestVersion := slices.Max(versions); latestVersion != expectedDbSchemaVersion {
var missingUpgrades []string
for version := latestVersion + 1; version <= expectedDbSchemaVersion; version++ {
if release, ok := schemaVersions[version]; ok {
missingUpgrades = append(missingUpgrades, release+".sql")
} else {
missingUpgrades = append(missingUpgrades, fmt.Sprintf("UNKNOWN (v%d)", version))
}
}

return fmt.Errorf("%w: v%d (expected v%d), "+
"please apply the following schema upgrade(s) to your database in order: %s "+
"(https://icinga.com/docs/icinga-db/latest/doc/04-Upgrading/)",
ErrSchemaMismatch, latestVersion, expectedDbSchemaVersion, strings.Join(missingUpgrades, ", "))
}

// Check if all schema updates between the oldest schema version and the expected version were applied.
for version := slices.Min(versions); version < expectedDbSchemaVersion; version++ {
if !slices.Contains(versions, version) {
release := "UNKNOWN"
if releaseVersion, ok := schemaVersions[version]; ok {
release = releaseVersion
}

return fmt.Errorf(
"%w: incomplete database schema upgrade: intermediate version v%d is missing,"+
" please make sure you have applied all database migrations after upgrading Icinga DB",
ErrSchemaMismatch, versions[i]+1)
"%w: incomplete database schema upgrade: intermediate version v%d (%s) is missing, "+
"please inspect the icingadb_schema database table and ensure that all database "+
"migrations were applied in order after upgrading Icinga DB",
ErrSchemaMismatch, version, release)
}
}

if latestVersion := versions[len(versions)-1]; latestVersion != expectedDbSchemaVersion {
// 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("%w: v%d (expected v%d), please make sure you have applied all database"+
" migrations after upgrading Icinga DB", ErrSchemaMismatch, latestVersion, expectedDbSchemaVersion,
)
// Extend the prior check by checking if the schema updates were applied in a monotonic increasing order.
// However, this returns an ErrSchemaImperfect error instead of an ErrSchemaMismatch.
for i := 0; i < len(versions)-1; i++ {
if versions[i] != versions[i+1]-1 {
return fmt.Errorf(
"%w: unexpected schema upgrade order after schema version %d, "+
"please inspect the icingadb_schema database table and ensure that all database "+
"migrations were applied in order after upgrading Icinga DB",
ErrSchemaImperfect, versions[i])
}
}

return nil
Expand Down
Loading