Merge branch 'feat/plugin-auto-upgrade' into deploy/dev
commit
db0090972e
@ -0,0 +1,41 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 16081485540c
|
||||||
|
Revises: d28f2004b072
|
||||||
|
Create Date: 2025-05-15 16:35:39.113777
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import models as models
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '16081485540c'
|
||||||
|
down_revision = 'd28f2004b072'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('tenant_plugin_auto_upgrade_strategies',
|
||||||
|
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
|
||||||
|
sa.Column('strategy_setting', sa.String(length=16), server_default='fix_only', nullable=False),
|
||||||
|
sa.Column('upgrade_time_of_day', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('upgrade_mode', sa.String(length=16), server_default='exclude', nullable=False),
|
||||||
|
sa.Column('exclude_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
|
||||||
|
sa.Column('include_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name='tenant_plugin_auto_upgrade_strategy_pkey'),
|
||||||
|
sa.UniqueConstraint('tenant_id', name='unique_tenant_plugin_auto_upgrade_strategy')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('tenant_plugin_auto_upgrade_strategies')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -0,0 +1,153 @@
|
|||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import app
|
||||||
|
from core.helper import marketplace
|
||||||
|
from core.plugin.entities.plugin import PluginInstallationSource
|
||||||
|
from core.plugin.impl.plugin import PluginInstaller
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.account import TenantPluginAutoUpgradeStrategy
|
||||||
|
|
||||||
|
AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL = 15 * 60 # 15 minutes
|
||||||
|
|
||||||
|
RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
|
||||||
|
|
||||||
|
|
||||||
|
@app.celery.task(queue="plugin")
|
||||||
|
def check_upgradable_plugin_task():
|
||||||
|
click.echo(click.style("Start check upgradable plugin.", fg="green"))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
now_seconds_of_day = time.time() % 86400 # we assume the tz is UTC
|
||||||
|
click.echo(click.style("Now seconds of day: {}".format(now_seconds_of_day), fg="green"))
|
||||||
|
|
||||||
|
# get strategies that set to be performed in the next AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL
|
||||||
|
strategies = (
|
||||||
|
db.session.query(TenantPluginAutoUpgradeStrategy)
|
||||||
|
.filter(
|
||||||
|
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day >= now_seconds_of_day,
|
||||||
|
TenantPluginAutoUpgradeStrategy.upgrade_time_of_day
|
||||||
|
< now_seconds_of_day + AUTO_UPGRADE_MINIMAL_CHECKING_INTERVAL,
|
||||||
|
TenantPluginAutoUpgradeStrategy.strategy_setting
|
||||||
|
!= TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED,
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = PluginInstaller()
|
||||||
|
|
||||||
|
for strategy in strategies:
|
||||||
|
try:
|
||||||
|
tenant_id = strategy.tenant_id
|
||||||
|
strategy_setting = strategy.strategy_setting
|
||||||
|
upgrade_mode = strategy.upgrade_mode
|
||||||
|
exclude_plugins = strategy.exclude_plugins
|
||||||
|
include_plugins = strategy.include_plugins
|
||||||
|
|
||||||
|
if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get plugins that need to be checked
|
||||||
|
plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
|
||||||
|
|
||||||
|
if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
|
||||||
|
all_plugins = manager.list_plugins(tenant_id)
|
||||||
|
|
||||||
|
for plugin in all_plugins:
|
||||||
|
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
|
||||||
|
plugin_ids.append((plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier))
|
||||||
|
|
||||||
|
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||||
|
# get all plugins and remove the exclude plugins
|
||||||
|
all_plugins = manager.list_plugins(tenant_id)
|
||||||
|
plugin_ids = [
|
||||||
|
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
|
||||||
|
for plugin in all_plugins
|
||||||
|
if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
|
||||||
|
]
|
||||||
|
elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||||
|
all_plugins = manager.list_plugins(tenant_id)
|
||||||
|
plugin_ids = [
|
||||||
|
(plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
|
||||||
|
for plugin in all_plugins
|
||||||
|
if plugin.source == PluginInstallationSource.Marketplace
|
||||||
|
]
|
||||||
|
|
||||||
|
if not plugin_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
plugin_ids_plain_list = [plugin_id for plugin_id, _, _ in plugin_ids]
|
||||||
|
|
||||||
|
click.echo(click.style("Fetching manifests for plugins: {}".format(plugin_ids_plain_list), fg="green"))
|
||||||
|
|
||||||
|
# fetch latest versions from marketplace
|
||||||
|
manifests = marketplace.batch_fetch_plugin_manifests(plugin_ids_plain_list)
|
||||||
|
|
||||||
|
for manifest in manifests:
|
||||||
|
for plugin_id, version, original_unique_identifier in plugin_ids:
|
||||||
|
if manifest.plugin_id != plugin_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_version = version
|
||||||
|
latest_version = manifest.latest_version
|
||||||
|
|
||||||
|
# @yeuoly review here
|
||||||
|
def fix_only_checker(latest_version, current_version):
|
||||||
|
latest_version_tuple = tuple(int(val) for val in latest_version.split("."))
|
||||||
|
current_version_tuple = tuple(int(val) for val in current_version.split("."))
|
||||||
|
|
||||||
|
if (
|
||||||
|
latest_version_tuple[0] == current_version_tuple[0]
|
||||||
|
and latest_version_tuple[1] == current_version_tuple[1]
|
||||||
|
):
|
||||||
|
return latest_version_tuple[2] != current_version_tuple[2]
|
||||||
|
return False
|
||||||
|
|
||||||
|
version_checker = {
|
||||||
|
TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: lambda latest_version,
|
||||||
|
current_version: latest_version != current_version,
|
||||||
|
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
|
||||||
|
}
|
||||||
|
|
||||||
|
if version_checker[strategy_setting](latest_version, current_version):
|
||||||
|
# execute upgrade
|
||||||
|
new_unique_identifier = manifest.latest_package_identifier
|
||||||
|
|
||||||
|
marketplace.record_install_plugin_event(new_unique_identifier)
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
"Upgrade plugin: {} -> {}".format(
|
||||||
|
original_unique_identifier, new_unique_identifier
|
||||||
|
),
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
task_start_resp = manager.upgrade_plugin(
|
||||||
|
tenant_id,
|
||||||
|
original_unique_identifier,
|
||||||
|
new_unique_identifier,
|
||||||
|
PluginInstallationSource.Marketplace,
|
||||||
|
{
|
||||||
|
"plugin_unique_identifier": new_unique_identifier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(click.style("Error when upgrading plugin: {}".format(e), fg="red"))
|
||||||
|
traceback.print_exc()
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(click.style("Error when checking upgradable plugin: {}".format(e), fg="red"))
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
"Checked upgradable plugin success latency: {}".format(end_at - start_at),
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models.account import TenantPluginAutoUpgradeStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class PluginAutoUpgradeService:
|
||||||
|
@staticmethod
|
||||||
|
def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
return (
|
||||||
|
session.query(TenantPluginAutoUpgradeStrategy)
|
||||||
|
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def change_strategy(
|
||||||
|
tenant_id: str,
|
||||||
|
strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
|
||||||
|
upgrade_time_of_day: int,
|
||||||
|
upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
|
||||||
|
exclude_plugins: list[str],
|
||||||
|
include_plugins: list[str],
|
||||||
|
) -> None:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
exist_strategy = (
|
||||||
|
session.query(TenantPluginAutoUpgradeStrategy)
|
||||||
|
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not exist_strategy:
|
||||||
|
strategy = TenantPluginAutoUpgradeStrategy(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
strategy_setting=strategy_setting,
|
||||||
|
upgrade_time_of_day=upgrade_time_of_day,
|
||||||
|
upgrade_mode=upgrade_mode,
|
||||||
|
exclude_plugins=exclude_plugins,
|
||||||
|
include_plugins=include_plugins,
|
||||||
|
)
|
||||||
|
session.add(strategy)
|
||||||
|
else:
|
||||||
|
exist_strategy.strategy_setting = strategy_setting
|
||||||
|
exist_strategy.upgrade_time_of_day = upgrade_time_of_day
|
||||||
|
exist_strategy.upgrade_mode = upgrade_mode
|
||||||
|
exist_strategy.exclude_plugins = exclude_plugins
|
||||||
|
exist_strategy.include_plugins = include_plugins
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exclude_plugin(tenant_id: str, plugin_id: str) -> bool:
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
exist_strategy = (
|
||||||
|
session.query(TenantPluginAutoUpgradeStrategy)
|
||||||
|
.filter(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not exist_strategy:
|
||||||
|
# create for this tenant
|
||||||
|
PluginAutoUpgradeService.change_strategy(
|
||||||
|
tenant_id,
|
||||||
|
TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY,
|
||||||
|
0,
|
||||||
|
TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE,
|
||||||
|
[plugin_id],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
|
||||||
|
if plugin_id not in exist_strategy.exclude_plugins:
|
||||||
|
new_exclude_plugins = exist_strategy.exclude_plugins.copy()
|
||||||
|
new_exclude_plugins.append(plugin_id)
|
||||||
|
exist_strategy.exclude_plugins = new_exclude_plugins
|
||||||
|
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL:
|
||||||
|
if plugin_id in exist_strategy.include_plugins:
|
||||||
|
new_include_plugins = exist_strategy.include_plugins.copy()
|
||||||
|
new_include_plugins.remove(plugin_id)
|
||||||
|
exist_strategy.include_plugins = new_include_plugins
|
||||||
|
elif exist_strategy.upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
|
||||||
|
exist_strategy.upgrade_mode = TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE
|
||||||
|
exist_strategy.exclude_plugins = [plugin_id]
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
return True
|
||||||
Loading…
Reference in New Issue