Skip to content

feat: auto-downgrading revision #4678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 123 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
408e7fc
first version
korolenkowork May 1, 2025
dce47b0
fix
korolenkowork May 1, 2025
6127d86
Main logic completed
korolenkowork May 1, 2025
8cb9b81
minor
korolenkowork May 1, 2025
9cc98a8
Update configuration.mdx
korolenkowork May 1, 2025
2758d9f
lint fix
korolenkowork May 1, 2025
e1ee04b
Add test
korolenkowork May 2, 2025
51787ba
fix path
korolenkowork May 2, 2025
db174f9
fix
korolenkowork May 2, 2025
9b2f67f
migrations-e2e-prepare-for-coding
korolenkowork May 2, 2025
3fb2996
migrations-e2e
korolenkowork May 2, 2025
08db877
Done!
korolenkowork May 2, 2025
4f635f3
fix pull old version e2e
korolenkowork May 2, 2025
f140de4
fix
korolenkowork May 3, 2025
995fd83
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
1d1e462
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
5dd3bfa
fix
korolenkowork May 3, 2025
cb46278
Update test-pr-e2e.yml
korolenkowork May 3, 2025
bf1b653
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
f469bab
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork May 3, 2025
3e3975c
Check revision between runs
korolenkowork May 3, 2025
4671d62
Merge branch 'feature/auto-downgrade-revision' of https://github.com/…
korolenkowork May 3, 2025
c55103a
fix execute container name
korolenkowork May 3, 2025
83b748d
test
korolenkowork May 3, 2025
f3ee8e3
test
korolenkowork May 3, 2025
9036754
test
korolenkowork May 3, 2025
e185781
test
korolenkowork May 3, 2025
33fbe21
test
korolenkowork May 3, 2025
3fcdfcb
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
b2beede
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
e4dd78a
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
8fd2137
revert
korolenkowork May 3, 2025
9545c5a
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
87d28d3
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
a2a1260
fix e2e env
korolenkowork May 3, 2025
cf0d105
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
1d34408
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
ff4b2e9
I hate pipelines
korolenkowork May 3, 2025
303e63c
Done!
korolenkowork May 3, 2025
394fd03
Add dummy migrations to the second step
korolenkowork May 3, 2025
a3a2735
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
fd45db1
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
4c96bcc
Fix docker cp path
korolenkowork May 3, 2025
b3a1190
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
197c98a
Update run-migrations-e2e-tests.yml
korolenkowork May 3, 2025
a38facc
Merge branch 'main' into feature/auto-downgrade-revision
shahargl May 4, 2025
8702a2b
Update run-migrations-e2e-tests.yml
korolenkowork May 4, 2025
0e208ef
Merge branch 'feature/auto-downgrade-revision' of https://github.com/…
korolenkowork May 4, 2025
32f2cbe
fix workflow
korolenkowork May 4, 2025
9d7ea1f
Remove test alembic config
korolenkowork May 4, 2025
b05a727
fix
korolenkowork May 4, 2025
099d9ea
fix
korolenkowork May 4, 2025
755f143
fix
korolenkowork May 4, 2025
8af5a68
fix
korolenkowork May 6, 2025
f83c3f0
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork May 6, 2025
c29ef1b
fix?
korolenkowork May 6, 2025
753f899
fix?
korolenkowork May 6, 2025
1cdc291
change SECRET_MANAGER_DIRECTORY
korolenkowork May 6, 2025
9d9791e
Delete docker-compose-e2e-postgres.yml~
korolenkowork May 6, 2025
beb812e
Revert "change SECRET_MANAGER_DIRECTORY"
korolenkowork May 6, 2025
5136a03
fix
korolenkowork May 6, 2025
2b3693a
Revert "fix"
korolenkowork May 6, 2025
775322f
fix
korolenkowork May 6, 2025
ef08091
fix
korolenkowork May 9, 2025
f1072e8
fix?
korolenkowork May 9, 2025
fa83f0f
revert
korolenkowork May 9, 2025
c24254c
fix
korolenkowork May 9, 2025
f40ed3c
test
korolenkowork May 9, 2025
6b9d5a0
test
korolenkowork May 9, 2025
0be6341
try
korolenkowork May 9, 2025
5224b8c
test
korolenkowork May 10, 2025
0870a1b
fix
korolenkowork May 10, 2025
a8d1acc
Revert "test"
korolenkowork May 10, 2025
e1f4a30
Update docker-compose-e2e-postgres.yml
korolenkowork May 10, 2025
8ced224
fix
korolenkowork May 10, 2025
42a362a
Update docker-compose-e2e-postgres.yml
korolenkowork May 10, 2025
1211b91
Mock final step migrations
korolenkowork May 10, 2025
c1e648f
Update poetry.lock
korolenkowork May 10, 2025
99be2fa
Update run-migrations-e2e-tests.yml
korolenkowork May 10, 2025
5e1e409
fix
korolenkowork May 10, 2025
7a405b8
Update run-migrations-e2e-tests.yml
korolenkowork May 10, 2025
fea2ead
Update run-migrations-e2e-tests.yml
korolenkowork May 10, 2025
ea6746e
Update run-migrations-e2e-tests.yml
korolenkowork May 10, 2025
4fdb76e
try
korolenkowork May 10, 2025
d458eac
test
korolenkowork May 10, 2025
4c79efe
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
1d2716c
minor changes in copy migrations logic
korolenkowork May 15, 2025
4adea39
Merge branch 'feature/auto-downgrade-revision' of https://github.com/…
korolenkowork May 15, 2025
e645a5e
fix
korolenkowork May 15, 2025
a1ffa55
test
korolenkowork May 15, 2025
f4d8ea8
fix
korolenkowork May 15, 2025
84ce798
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
36051a0
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
d41c554
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
68416f8
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
4a6039e
stop instead of down
korolenkowork May 15, 2025
9eeaddd
Update docker-compose-e2e-postgres.yml
korolenkowork May 15, 2025
24200a7
final
korolenkowork May 15, 2025
e7ea523
final
korolenkowork May 15, 2025
70325a0
fix
korolenkowork May 15, 2025
1725ae2
fix test script path
korolenkowork May 15, 2025
166abba
fix
korolenkowork May 15, 2025
70808cc
fix
korolenkowork May 15, 2025
89868ba
Ahhhh
korolenkowork May 15, 2025
4faabfb
Merge branch 'main' into feature/auto-downgrade-revision
shahargl May 19, 2025
e1e1dad
Post review fixes
korolenkowork May 19, 2025
40a8340
Merge branch 'feature/auto-downgrade-revision' of https://github.com/…
korolenkowork May 19, 2025
ad148d8
fix
korolenkowork May 19, 2025
7afb934
Update docker-compose-e2e-postgres.yml
korolenkowork May 19, 2025
3f1f88a
Merge branch 'main' into feature/auto-downgrade-revision
shahargl May 19, 2025
49df674
fix
korolenkowork May 19, 2025
077ba69
Merge branch 'feature/auto-downgrade-revision' of https://github.com/…
korolenkowork May 19, 2025
ed91ff4
Add migration path to dockerfiles
korolenkowork May 19, 2025
f1328ef
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork May 22, 2025
bc5ebce
Merge branch 'main' into feature/auto-downgrade-revision
shahargl May 25, 2025
02ca8bf
Merge branch 'main' into feature/auto-downgrade-revision
shahargl May 26, 2025
db76ae7
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork May 29, 2025
c05e35e
Make separate docker compose for migrations e2e
korolenkowork May 29, 2025
aea9b81
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork Jun 9, 2025
356ce18
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork Jun 14, 2025
13fe761
Update poetry.lock
korolenkowork Jun 16, 2025
96d9479
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork Jun 27, 2025
71e81be
Merge branch 'main' into feature/auto-downgrade-revision
korolenkowork Jul 3, 2025
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
1 change: 1 addition & 0 deletions docker-compose.common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
- SECRET_MANAGER_DIRECTORY=/state
- DATABASE_CONNECTION_STRING=sqlite:////state/db.sqlite3?check_same_thread=False
- OPENAI_API_KEY=$OPENAI_API_KEY
- ALLOW_DB_DOWNGRADE=false
- PUSHER_APP_ID=1
- PUSHER_APP_KEY=keepappkey
- PUSHER_APP_SECRET=keepappsecret
Expand Down
1 change: 1 addition & 0 deletions docs/deployment/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Keep is highly configurable through environment variables. This allows you to cu
| **DB_SERVICE_ACCOUNT** | Service account for database impersonation | No | None | Valid service account email |
| **DB_IP_TYPE** | Specifies the Cloud SQL IP type | No | "public" | "public", "private" or "psc" |
| **SKIP_DB_CREATION** | Skips database creation and migrations | No | "false" | "true" or "false" |
| **ALLOW_DB_DOWNGRADE** | Enables downgrading database schema | No | "false" | "true" or "false" |

### Resource Provisioning

Expand Down
77 changes: 72 additions & 5 deletions keep/api/core/db_on_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import hashlib
import logging
import os
import shutil

import alembic.command
import alembic.config
from alembic.runtime.migration import MigrationContext
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select

Expand Down Expand Up @@ -167,23 +169,88 @@ def try_create_single_tenant(tenant_id: str, create_default_user=True) -> None:
logger.exception("Failed to create single tenant")
pass

def get_current_revision():
"""Get current app revision"""
with engine.connect() as connection:
context = MigrationContext.configure(connection)
return context.get_current_revision()

def migrate_db():
def copy_migrations(app_migrations_path, local_migrations_path):
"""Copy migrations to a local backup folder for safe downgrade purposes."""
source_versions_path = os.path.join(app_migrations_path, "versions")

# If the destination folder already exists, remove it
if os.path.exists(local_migrations_path):
shutil.rmtree(local_migrations_path)

# Copy the entire directory tree
shutil.copytree(source_versions_path, local_migrations_path)

def downgrade_db(config, expected_revision, local_migrations_path, app_migrations_path):
"""
Downgrade the DB to the previous revision.
"""
source_versions_path = os.path.join(app_migrations_path, "versions")
source_versions_path_copy = os.path.join(app_migrations_path, "versions_copy")

# Rename the source versions folder before restoring migrations from the backup folder
shutil.move(source_versions_path, source_versions_path_copy)

# Restore migrations from the backup folder
shutil.copytree(local_migrations_path, source_versions_path)

# Downgrade database from backup migrations
alembic.command.downgrade(config, expected_revision)

# Restoring source migrations
shutil.rmtree(source_versions_path)
shutil.move(source_versions_path_copy, source_versions_path)

def migrate_db(config_path: str = None, app_migrations_path: str = None):
"""
Run migrations to make sure the DB is up-to-date.
"""
if os.environ.get("SKIP_DB_CREATION", "false") == "true":
logger.info("Skipping running migrations...")
return None

logger.info("Running migrations...")
config_path = os.path.dirname(os.path.abspath(__file__)) + "/../../" + "alembic.ini"
config_path = config_path or os.path.dirname(os.path.abspath(__file__)) + "/../../" + "alembic.ini"
config = alembic.config.Config(file_=config_path)
# Re-defined because alembic.ini uses relative paths which doesn't work
# when running the app as a pyhton pakage (could happen form any path)

# This path will be used to save migrations locally for safe downgrade purposes
local_migrations_path = os.environ.get("SECRET_MANAGER_DIRECTORY", "/state") + "/migrations"
app_migrations_path = app_migrations_path or os.path.dirname(os.path.abspath(__file__)) + "/../models/db/migrations"
config.set_main_option(
"script_location",
os.path.dirname(os.path.abspath(__file__)) + "/../models/db/migrations",
app_migrations_path,
)
alembic.command.upgrade(config, "head")
alembic_script = alembic.script.ScriptDirectory.from_config(config)

current_revision = get_current_revision()
expected_revision = alembic_script.get_current_head()

# If the current revision is the same as the expected revision, we don't need to run migrations
if current_revision and expected_revision and current_revision == expected_revision:
logger.info("Database schema is up-to-date!")
return None

logger.warning(f"Database schema ({current_revision}) doesn't match application version ({expected_revision})")
logger.info("Running migrations...")
try:
alembic.command.upgrade(config, "head")
except Exception as e:
logger.error(f"{e} it's seems like KeepHQ was rolled back to a previous version")

if not os.getenv("ALLOW_DB_DOWNGRADE", "false") == "true":
logger.error(f"ALLOW_DB_DOWNGRADE is not set to true, but the database schema ({current_revision}) doesn't match application version ({expected_revision})")
raise RuntimeError("Database downgrade is not allowed")

logger.info("Downgrading database schema...")
downgrade_db(config, expected_revision, local_migrations_path, app_migrations_path)

# Copy migrations to local folder for safe downgrade purposes
copy_migrations(app_migrations_path, local_migrations_path)

logger.info("Finished migrations")
Empty file added tests/migrations/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions tests/migrations/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[alembic]
# Re-defined in the keep/api/core/db_on_start.py to make it stable while keep is installed as a package
script_location = keep/api/models/db/migrations
file_template = %%(year)d-%%(month).2d-%%(day).2d-%%(hour).2d-%%(minute).2d_%%(rev)s
prepend_sys_path = .
output_encoding = utf-8


[post_write_hooks]
hooks = black,isort

black.type = console_scripts
black.entrypoint = black

isort.type = console_scripts
isort.entrypoint = isort

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [PID %(process)d] [%(name)s] %(message)s
datefmt = %H:%M:%S
136 changes: 136 additions & 0 deletions tests/migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import asyncio
import os
from logging.config import fileConfig

from alembic import context
from alembic.script import ScriptDirectory
from sqlalchemy import create_engine, StaticPool
from sqlalchemy.future import Connection
from sqlmodel import SQLModel

import keep.api.logging
from keep.api.core.db_utils import create_db_engine
from keep.api.models.db.action import *
from keep.api.models.db.ai_suggestion import *
from keep.api.models.db.alert import *
from keep.api.models.db.dashboard import *
from keep.api.models.db.extraction import *
from keep.api.models.db.facet import *
from keep.api.models.db.maintenance_window import *
from keep.api.models.db.mapping import *
from keep.api.models.db.preset import *
from keep.api.models.db.provider import *
from keep.api.models.db.rule import *
from keep.api.models.db.statistics import *
from keep.api.models.db.tenant import *
from keep.api.models.db.topology import *
from keep.api.models.db.user import *
from keep.api.models.db.workflow import *

target_metadata = SQLModel.metadata

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
# backup the current config
logging_config = config.get_section("loggers")
fileConfig(config.config_file_name)


async def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
connectable = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
context.configure(
url=str(connectable.url),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True,
)

with context.begin_transaction():
context.run_migrations()


def do_run_migrations(connection: Connection) -> None:
"""
Run actual sync migrations.

:param connection: connection to the database.
"""
context.configure(
connection=connection, target_metadata=target_metadata, render_as_batch=True
)

with context.begin_transaction():
context.run_migrations()


async def run_migrations_online() -> None:
"""
Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = create_db_engine()
try:
do_run_migrations(connectable.connect())
except Exception as e:
# print all migrations so we will know what failed
list_migrations(connectable)
raise e


def list_migrations(connectable):
"""
List all migrations and their status for debugging.
"""
try:
# Get the script directory from the alembic context
script_directory = ScriptDirectory.from_config(config)
current_rev = script_directory.get_current_head()
# List all available migrations
pid = os.getpid()
print(f"[{pid}] Available migrations:")
try:
for script in script_directory.walk_revisions():
status = (
"PENDING"
if current_rev and script.revision > current_rev
else "APPLIED"
)
print(f" - {script.revision}: {script.doc} ({status})")
except Exception as exc:
logger.exception(f"Failed to list migrations: {exc}")
except Exception as exc:
logger.exception(f"Failed to process migration information: {exc}")


loop = asyncio.get_event_loop()
if context.is_offline_mode():
task = run_migrations_offline()
else:
task = run_migrations_online()

loop.run_until_complete(task)
# SHAHAR: set back the logs to the default after alembic is done
keep.api.logging.setup_logging()
27 changes: 27 additions & 0 deletions tests/migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel
import sqlalchemy_utils

${imports if imports else ""}

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy