CheckSchema: Verify intermediate schema upgrades

When skipping a version for an Icinga DB upgrade, all intermediate
upgrade steps must be taken. While this is already stated in the
documentation, it might be overlooked.

This happened for one community user, upgrading from v1.1.0 to v1.2.0,
skipping the intermediate schema upgrade for v1.1.1.

> https://community.icinga.com/t/icingadb-failing-exactly-5-minutes-after-start/13955

First, the necessity for all upgrades in their release order was made
more prominent in the documentation, hoping that less users would ignore
this when skimming the upgrade docs.

However, the real change here is adding another check to the CheckSchema
function, verifying that all schema upgrades between the lowest known
version and the highest known version in the icingadb_schema table
exists. If an intermediate schema upgrade was skipped, as in the thread
above, this raises a descriptive error.
This commit is contained in:
Alvar Penning 2024-08-26 09:44:56 +02:00
parent 8b64be8197
commit f49fac0798
No known key found for this signature in database
2 changed files with 24 additions and 6 deletions

View file

@ -4,7 +4,8 @@ Some Icinga DB upgrades require manual intervention, others do not. If you need
point you to the specific upgrade section on this page.
Please note that version upgrades are incremental. If you are upgrading across multiple versions, make sure to follow
the steps for each of them.
the steps for each of them. For example, when upgrading from version 1.1.0 to 1.2.0, follow all instructions for
upgrading to 1.1.1, then all for 1.2.0, including schema upgrades.
## Database Schema Upgrades

View file

@ -48,12 +48,13 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
return ErrSchemaNotExists
}
var version uint16
var versions []uint16
err := retry.WithBackoff(
ctx,
func(ctx context.Context) error {
query := "SELECT version FROM icingadb_schema ORDER BY id DESC LIMIT 1"
if err := db.QueryRowxContext(ctx, query).Scan(&version); err != nil {
query := "SELECT version FROM icingadb_schema ORDER BY version ASC"
if err := db.SelectContext(ctx, &versions, query); err != nil {
return database.CantPerformQuery(err, query)
}
return nil
@ -65,12 +66,28 @@ func CheckSchema(ctx context.Context, db *database.DB) error {
return errors.Wrap(err, "can't check database schema version")
}
if version != expectedDbSchemaVersion {
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 {
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)
}
}
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, version, expectedDbSchemaVersion,
" migrations after upgrading Icinga DB", ErrSchemaMismatch, latestVersion, expectedDbSchemaVersion,
)
}