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
4 changes: 4 additions & 0 deletions cmd/icinga-notifications/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func main() {
logger.Fatalf("Cannot connect to the database: %+v", err)
}

if err := internal.CheckSchema(ctx, db); err != nil {
logger.Fatalf("%+v", err)
}

channel.UpsertPlugins(ctx, conf.ChannelsDir, logs.GetChildLogger("channel"), db)

runtimeConfig := config.NewRuntimeConfig(logs, db)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/teambition/rrule-go v1.8.2
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.51.0
golang.org/x/mod v0.35.0
golang.org/x/sync v0.20.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
Expand Down
71 changes: 71 additions & 0 deletions internal/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package internal

import (
"context"
"errors"
"fmt"

"github.com/icinga/icinga-go-library/backoff"
"github.com/icinga/icinga-go-library/database"
"github.com/icinga/icinga-go-library/retry"
"golang.org/x/mod/semver"
)

const (
expectedMysqlSchemaVersion = "v1.0"
expectedPostgresSchemaVersion = "v1.0"
)

// CheckSchema verifies that the database schema version matches the expected version for the database driver.
func CheckSchema(ctx context.Context, db *database.DB) error {
var expectedSchemaVersion string
switch db.DriverName() {
case database.MySQL:
expectedSchemaVersion = expectedMysqlSchemaVersion
case database.PostgreSQL:
expectedSchemaVersion = expectedPostgresSchemaVersion
default:
return fmt.Errorf("unsupported database driver %q", db.DriverName())
}

if hasSchemaTable, err := db.HasTable(ctx, "notifications_schema"); err != nil {
return fmt.Errorf("cannot verify existence of database schema table: %w", err)
} else if !hasSchemaTable {
return errors.New("notifications_schema table does not exist")
}

var dbResult []string
err := retry.WithBackoff(
ctx,
func(ctx context.Context) error {
qs := `SELECT version FROM notifications_schema ORDER BY timestamp DESC LIMIT 1`
if err := db.SelectContext(ctx, &dbResult, qs); err != nil {
return database.CantPerformQuery(err, qs)
}
return nil
},
retry.Retryable,
backoff.DefaultBackoff,
db.GetDefaultRetrySettings(),
)
if err != nil {
return err
}

if len(dbResult) == 0 {
return errors.New("no database schema version found")
}

// Compare the actual schema version with the expected version using semantic versioning comparison.
// A simple string comparison could have been enough in most cases, but using semver allows for more
// flexible versioning, so that `v1`, `v1.0`, and `v1.0.0` are all considered equivalent.
if actualSchemaVersion := dbResult[0]; semver.Compare(expectedSchemaVersion, actualSchemaVersion) != 0 {
return fmt.Errorf(
"unexpected database schema version: %s (expected %s), please make sure you have applied all"+
" database migrations after upgrading Icinga Notifications",
actualSchemaVersion, expectedSchemaVersion,
)
}

return nil
}
37 changes: 37 additions & 0 deletions schema/mysql/schema.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
DROP PROCEDURE IF EXISTS assert_correct_schema_version;
DELIMITER //
-- This procedure can be used in upgrade scripts to assert that the schema version in the database matches the
-- expected version before applying the upgrade. This is important to prevent users from accidentally skipping
-- intermediate upgrade scripts, which could lead to an inconsistent database state. For instance, since every
-- upgrade script knows its predecessor's version, we can just do "CALL assert_correct_schema_version('v1.0')"
-- at the beginning of the 1.x upgrade scripts to ensure that the 1.0 script has been applied before.
CREATE PROCEDURE assert_correct_schema_version(expected_version text)
READS SQL DATA
COMMENT 'Asserts that the schema version in the database matches the expected version and raises an error if not.'
BEGIN
DECLARE actual_version text;
DECLARE error_message text;
SELECT version INTO actual_version FROM notifications_schema ORDER BY timestamp DESC LIMIT 1;
IF actual_version IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Schema version not found in notifications_schema table.';
ELSEIF actual_version != expected_version THEN
-- MySQL/MariaDB doesn't seem to allow to directly use CONCAT in the SIGNAL statement[^1],
-- so we need to set it to a variable first.
-- [^1]: https://bugs.mysql.com/bug.php?id=114001
SET error_message = CONCAT('Schema version mismatch: expected ', expected_version, ', got ', actual_version, '. Please apply all previous upgrade scripts in order before applying this one.');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = error_message;
END IF;
END;
//
DELIMITER ;

CREATE TABLE available_channel_type (
type varchar(255) NOT NULL,
name text NOT NULL,
Expand Down Expand Up @@ -453,3 +480,13 @@ CREATE TABLE browser_session (

CREATE INDEX idx_browser_session_authenticated_at ON browser_session (authenticated_at DESC);
CREATE INDEX idx_browser_session_username_agent ON browser_session (username, user_agent(512));

CREATE TABLE notifications_schema (
id int NOT NULL AUTO_INCREMENT,
version varchar(255) NOT NULL,
timestamp bigint NOT NULL,

CONSTRAINT pk_notifications_schema PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO notifications_schema(version, timestamp) VALUES('v1.0', UNIX_TIMESTAMP() * 1000);
33 changes: 33 additions & 0 deletions schema/mysql/upgrades/1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
DROP PROCEDURE IF EXISTS assert_correct_schema_version;
DELIMITER //
-- This procedure can be used in upgrade scripts to assert that the schema version in the database matches the
-- expected version before applying the upgrade. This is important to prevent users from accidentally skipping
-- intermediate upgrade scripts, which could lead to an inconsistent database state. For instance, since every
-- upgrade script knows its predecessor's version, we can just do "CALL assert_correct_schema_version('v1.0')"
-- at the beginning of the 1.x upgrade scripts to ensure that the 1.0 script has been applied before.
CREATE PROCEDURE assert_correct_schema_version(expected_version text)
READS SQL DATA
COMMENT 'Asserts that the schema version in the database matches the expected version and raises an error if not.'
BEGIN
DECLARE actual_version text;
DECLARE error_message text;
SELECT version INTO actual_version FROM notifications_schema ORDER BY timestamp DESC LIMIT 1;
IF actual_version IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Schema version not found in notifications_schema table.';
ELSEIF actual_version != expected_version THEN
SET error_message = CONCAT('Schema version mismatch: expected ', expected_version, ', got ', actual_version, '. Please apply all previous upgrade scripts in order before applying this one.');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = error_message;
END IF;
END;
//
DELIMITER ;

CREATE TABLE notifications_schema (
id int NOT NULL AUTO_INCREMENT,
version varchar(255) NOT NULL,
timestamp bigint NOT NULL,

CONSTRAINT pk_notifications_schema PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO notifications_schema(version, timestamp) VALUES('v1.0', UNIX_TIMESTAMP() * 1000);
31 changes: 31 additions & 0 deletions schema/pgsql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ CREATE OR REPLACE FUNCTION anynonarrayliketext(anynonarray, text)
$$;
CREATE OPERATOR ~~ (LEFTARG=anynonarray, RIGHTARG=text, PROCEDURE=anynonarrayliketext);

-- This procedure can be used in upgrade scripts to assert that the schema version in the database matches the
-- expected version before applying the upgrade. This is important to prevent users from accidentally skipping
-- intermediate upgrade scripts, which could lead to an inconsistent database state. For instance, since every
-- upgrade script knows its predecessor's version, we can just do "CALL assert_correct_schema_version('v1.0')"
-- at the beginning of the 1.x upgrade scripts to ensure that the 1.0 script has been applied before.
CREATE OR REPLACE PROCEDURE assert_correct_schema_version(expected_version text)
LANGUAGE plpgsql
AS $$
DECLARE
actual_version text;
BEGIN
SELECT version INTO actual_version FROM notifications_schema ORDER BY timestamp DESC LIMIT 1;
IF actual_version IS NULL THEN
RAISE 'Schema version not found in notifications_schema table.';
ELSIF actual_version != expected_version THEN
RAISE 'Schema version mismatch: expected %, got %. Please apply all previous upgrade scripts in order before applying this one.', expected_version, actual_version;
END IF;
END;
$$;
COMMENT ON PROCEDURE assert_correct_schema_version IS 'Asserts that the schema version in the database matches the expected version and raises an error if not.';

CREATE TABLE available_channel_type (
type varchar(255) NOT NULL,
name text NOT NULL,
Expand Down Expand Up @@ -499,3 +520,13 @@ CREATE TABLE browser_session (

CREATE INDEX idx_browser_session_authenticated_at ON browser_session (authenticated_at DESC);
CREATE INDEX idx_browser_session_username_agent ON browser_session (username, user_agent);

CREATE TABLE notifications_schema (
id serial,
version varchar(255) NOT NULL,
timestamp bigint NOT NULL,

CONSTRAINT pk_notifications_schema PRIMARY KEY (id)
);

INSERT INTO notifications_schema(version, timestamp) VALUES('v1.0', EXTRACT(EPOCH from NOW()) * 1000);
30 changes: 30 additions & 0 deletions schema/pgsql/upgrades/1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- This procedure can be used in upgrade scripts to assert that the schema version in the database matches the
-- expected version before applying the upgrade. This is important to prevent users from accidentally skipping
-- intermediate upgrade scripts, which could lead to an inconsistent database state. For instance, since every
-- upgrade script knows its predecessor's version, we can just do "CALL assert_correct_schema_version('v1.0')"
-- at the beginning of the 1.x upgrade scripts to ensure that the 1.0 script has been applied before.
CREATE OR REPLACE PROCEDURE assert_correct_schema_version(expected_version text)
LANGUAGE plpgsql
AS $$
DECLARE
actual_version text;
BEGIN
SELECT version INTO actual_version FROM notifications_schema ORDER BY timestamp DESC LIMIT 1;
IF actual_version IS NULL THEN
RAISE 'Schema version not found in notifications_schema table.';
ELSIF actual_version != expected_version THEN
RAISE 'Schema version mismatch: expected %, got %. Please apply all previous upgrade scripts in order before applying this one.', expected_version, actual_version;
END IF;
END;
$$;
COMMENT ON PROCEDURE assert_correct_schema_version IS 'Asserts that the schema version in the database matches the expected version and raises an error if not.';

CREATE TABLE notifications_schema (
id serial,
version varchar(255) NOT NULL,
timestamp bigint NOT NULL,

CONSTRAINT pk_notifications_schema PRIMARY KEY (id)
);

INSERT INTO notifications_schema(version, timestamp) VALUES('v1.0', EXTRACT(EPOCH from NOW()) * 1000);
Loading