You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

process_tenant_plugin_autoupgrade_check_task.py 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import json
  2. import operator
  3. import typing
  4. import click
  5. from celery import shared_task
  6. from core.helper import marketplace
  7. from core.helper.marketplace import MarketplacePluginDeclaration
  8. from core.plugin.entities.plugin import PluginInstallationSource
  9. from core.plugin.impl.plugin import PluginInstaller
  10. from extensions.ext_redis import redis_client
  11. from models.account import TenantPluginAutoUpgradeStrategy
  12. RETRY_TIMES_OF_ONE_PLUGIN_IN_ONE_TENANT = 3
  13. CACHE_REDIS_KEY_PREFIX = "plugin_autoupgrade_check_task:cached_plugin_manifests:"
  14. CACHE_REDIS_TTL = 60 * 15 # 15 minutes
  15. def _get_redis_cache_key(plugin_id: str) -> str:
  16. """Generate Redis cache key for plugin manifest."""
  17. return f"{CACHE_REDIS_KEY_PREFIX}{plugin_id}"
  18. def _get_cached_manifest(plugin_id: str) -> typing.Union[MarketplacePluginDeclaration, None, bool]:
  19. """
  20. Get cached plugin manifest from Redis.
  21. Returns:
  22. - MarketplacePluginDeclaration: if found in cache
  23. - None: if cached as not found (marketplace returned no result)
  24. - False: if not in cache at all
  25. """
  26. try:
  27. key = _get_redis_cache_key(plugin_id)
  28. cached_data = redis_client.get(key)
  29. if cached_data is None:
  30. return False
  31. cached_json = json.loads(cached_data)
  32. if cached_json is None:
  33. return None
  34. return MarketplacePluginDeclaration.model_validate(cached_json)
  35. except Exception:
  36. return False
  37. def _set_cached_manifest(plugin_id: str, manifest: typing.Union[MarketplacePluginDeclaration, None]) -> None:
  38. """
  39. Cache plugin manifest in Redis.
  40. Args:
  41. plugin_id: The plugin ID
  42. manifest: The manifest to cache, or None if not found in marketplace
  43. """
  44. try:
  45. key = _get_redis_cache_key(plugin_id)
  46. if manifest is None:
  47. # Cache the fact that this plugin was not found
  48. redis_client.setex(key, CACHE_REDIS_TTL, json.dumps(None))
  49. else:
  50. # Cache the manifest data
  51. redis_client.setex(key, CACHE_REDIS_TTL, manifest.model_dump_json())
  52. except Exception:
  53. # If Redis fails, continue without caching
  54. # traceback.print_exc()
  55. pass
  56. def marketplace_batch_fetch_plugin_manifests(
  57. plugin_ids_plain_list: list[str],
  58. ) -> list[MarketplacePluginDeclaration]:
  59. """Fetch plugin manifests with Redis caching support."""
  60. cached_manifests: dict[str, typing.Union[MarketplacePluginDeclaration, None]] = {}
  61. not_cached_plugin_ids: list[str] = []
  62. # Check Redis cache for each plugin
  63. for plugin_id in plugin_ids_plain_list:
  64. cached_result = _get_cached_manifest(plugin_id)
  65. if cached_result is False:
  66. # Not in cache, need to fetch
  67. not_cached_plugin_ids.append(plugin_id)
  68. else:
  69. # Either found manifest or cached as None (not found in marketplace)
  70. # At this point, cached_result is either MarketplacePluginDeclaration or None
  71. if isinstance(cached_result, bool):
  72. # This should never happen due to the if condition above, but for type safety
  73. continue
  74. cached_manifests[plugin_id] = cached_result
  75. # Fetch uncached plugins from marketplace
  76. if not_cached_plugin_ids:
  77. manifests = marketplace.batch_fetch_plugin_manifests_ignore_deserialization_error(not_cached_plugin_ids)
  78. # Cache the fetched manifests
  79. for manifest in manifests:
  80. cached_manifests[manifest.plugin_id] = manifest
  81. _set_cached_manifest(manifest.plugin_id, manifest)
  82. # Cache plugins that were not found in marketplace
  83. fetched_plugin_ids = {manifest.plugin_id for manifest in manifests}
  84. for plugin_id in not_cached_plugin_ids:
  85. if plugin_id not in fetched_plugin_ids:
  86. cached_manifests[plugin_id] = None
  87. _set_cached_manifest(plugin_id, None)
  88. # Build result list from cached manifests
  89. result: list[MarketplacePluginDeclaration] = []
  90. for plugin_id in plugin_ids_plain_list:
  91. cached_manifest: typing.Union[MarketplacePluginDeclaration, None] = cached_manifests.get(plugin_id)
  92. if cached_manifest is not None:
  93. result.append(cached_manifest)
  94. return result
  95. @shared_task(queue="plugin")
  96. def process_tenant_plugin_autoupgrade_check_task(
  97. tenant_id: str,
  98. strategy_setting: TenantPluginAutoUpgradeStrategy.StrategySetting,
  99. upgrade_time_of_day: int,
  100. upgrade_mode: TenantPluginAutoUpgradeStrategy.UpgradeMode,
  101. exclude_plugins: list[str],
  102. include_plugins: list[str],
  103. ):
  104. try:
  105. manager = PluginInstaller()
  106. click.echo(
  107. click.style(
  108. f"Checking upgradable plugin for tenant: {tenant_id}",
  109. fg="green",
  110. )
  111. )
  112. if strategy_setting == TenantPluginAutoUpgradeStrategy.StrategySetting.DISABLED:
  113. return
  114. # get plugin_ids to check
  115. plugin_ids: list[tuple[str, str, str]] = [] # plugin_id, version, unique_identifier
  116. click.echo(click.style(f"Upgrade mode: {upgrade_mode}", fg="green"))
  117. if upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.PARTIAL and include_plugins:
  118. all_plugins = manager.list_plugins(tenant_id)
  119. for plugin in all_plugins:
  120. if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id in include_plugins:
  121. plugin_ids.append(
  122. (
  123. plugin.plugin_id,
  124. plugin.version,
  125. plugin.plugin_unique_identifier,
  126. )
  127. )
  128. elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.EXCLUDE:
  129. # get all plugins and remove excluded plugins
  130. all_plugins = manager.list_plugins(tenant_id)
  131. plugin_ids = [
  132. (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
  133. for plugin in all_plugins
  134. if plugin.source == PluginInstallationSource.Marketplace and plugin.plugin_id not in exclude_plugins
  135. ]
  136. elif upgrade_mode == TenantPluginAutoUpgradeStrategy.UpgradeMode.ALL:
  137. all_plugins = manager.list_plugins(tenant_id)
  138. plugin_ids = [
  139. (plugin.plugin_id, plugin.version, plugin.plugin_unique_identifier)
  140. for plugin in all_plugins
  141. if plugin.source == PluginInstallationSource.Marketplace
  142. ]
  143. if not plugin_ids:
  144. return
  145. plugin_ids_plain_list = [plugin_id for plugin_id, _, _ in plugin_ids]
  146. manifests = marketplace_batch_fetch_plugin_manifests(plugin_ids_plain_list)
  147. if not manifests:
  148. return
  149. for manifest in manifests:
  150. for plugin_id, version, original_unique_identifier in plugin_ids:
  151. if manifest.plugin_id != plugin_id:
  152. continue
  153. try:
  154. current_version = version
  155. latest_version = manifest.latest_version
  156. def fix_only_checker(latest_version: str, current_version: str):
  157. latest_version_tuple = tuple(int(val) for val in latest_version.split("."))
  158. current_version_tuple = tuple(int(val) for val in current_version.split("."))
  159. if (
  160. latest_version_tuple[0] == current_version_tuple[0]
  161. and latest_version_tuple[1] == current_version_tuple[1]
  162. ):
  163. return latest_version_tuple[2] != current_version_tuple[2]
  164. return False
  165. version_checker = {
  166. TenantPluginAutoUpgradeStrategy.StrategySetting.LATEST: operator.ne,
  167. TenantPluginAutoUpgradeStrategy.StrategySetting.FIX_ONLY: fix_only_checker,
  168. }
  169. if version_checker[strategy_setting](latest_version, current_version):
  170. # execute upgrade
  171. new_unique_identifier = manifest.latest_package_identifier
  172. marketplace.record_install_plugin_event(new_unique_identifier)
  173. click.echo(
  174. click.style(
  175. f"Upgrade plugin: {original_unique_identifier} -> {new_unique_identifier}",
  176. fg="green",
  177. )
  178. )
  179. _ = manager.upgrade_plugin(
  180. tenant_id,
  181. original_unique_identifier,
  182. new_unique_identifier,
  183. PluginInstallationSource.Marketplace,
  184. {
  185. "plugin_unique_identifier": new_unique_identifier,
  186. },
  187. )
  188. except Exception as e:
  189. click.echo(click.style(f"Error when upgrading plugin: {e}", fg="red"))
  190. # traceback.print_exc()
  191. break
  192. except Exception as e:
  193. click.echo(click.style(f"Error when checking upgradable plugin: {e}", fg="red"))
  194. # traceback.print_exc()
  195. return