plugin app manager, app manager docker: update implementation:
A new `application_update` bridge method is added to easily update images. Running container are automatically updated if necessary.
This commit is contained in:
parent
6119c37c0f
commit
6b4f7252e3
3 changed files with 144 additions and 7 deletions
|
|
@ -17,12 +17,21 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field
|
||||
from twisted.python.procutils import which
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.constants import Const as C
|
||||
|
||||
from libervia.backend import G
|
||||
from libervia.backend.core import exceptions
|
||||
from libervia.backend.core.constants import Const as C
|
||||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.plugins.plugin_misc_app_manager.models import AppManagerBackend
|
||||
from libervia.backend.plugins.plugin_misc_app_manager.models import (
|
||||
AppManagerBackend,
|
||||
AppUpdateResult,
|
||||
AppUpdateServiceInfo,
|
||||
)
|
||||
from libervia.backend.tools.common import async_process
|
||||
from libervia.backend.tools.config import PluginSettings
|
||||
|
||||
|
|
@ -108,6 +117,63 @@ class AppManagerDocker(AppManagerBackend):
|
|||
path=str(working_dir),
|
||||
)
|
||||
|
||||
async def update(self, app_data: dict) -> AppUpdateResult | None:
|
||||
working_dir = app_data["_instance_dir_path"]
|
||||
old_versions = await self._get_images(working_dir)
|
||||
await async_process.run(
|
||||
self.runner_path,
|
||||
"compose",
|
||||
"pull",
|
||||
path=str(working_dir),
|
||||
)
|
||||
new_versions = await self._get_images(working_dir)
|
||||
updated = [
|
||||
AppUpdateServiceInfo(
|
||||
service=name, old_version=old_id, new_version=new_versions[name]
|
||||
)
|
||||
for name, old_id in old_versions.items()
|
||||
if name in new_versions and old_id != new_versions[name]
|
||||
]
|
||||
if not updated:
|
||||
return None
|
||||
if app_data.get("_started"):
|
||||
# Running: recreate containers with new images
|
||||
await async_process.run(
|
||||
self.runner_path,
|
||||
"compose",
|
||||
"up",
|
||||
"--detach",
|
||||
path=str(working_dir),
|
||||
)
|
||||
elif old_versions:
|
||||
# Stopped: recreate containers but don't start them
|
||||
await async_process.run(
|
||||
self.runner_path,
|
||||
"compose",
|
||||
"up",
|
||||
"--detach",
|
||||
"--no-start",
|
||||
path=str(working_dir),
|
||||
)
|
||||
return AppUpdateResult(updated=updated)
|
||||
|
||||
async def _get_images(self, working_dir: Path) -> dict[str, str]:
|
||||
output = await async_process.run(
|
||||
self.runner_path,
|
||||
"compose",
|
||||
"images",
|
||||
"--format",
|
||||
"{{.Name}}:{{.Image}}",
|
||||
path=str(working_dir),
|
||||
)
|
||||
images: dict[str, str] = {}
|
||||
for line in output.decode().strip().splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
name, image = line.split(":", 1)
|
||||
images[name] = image
|
||||
return images
|
||||
|
||||
async def compute_expose(self, app_data: dict) -> None:
|
||||
working_dir = app_data["_instance_dir_path"]
|
||||
expose = app_data["expose"]
|
||||
|
|
@ -132,7 +198,7 @@ class AppManagerDocker(AppManagerBackend):
|
|||
)
|
||||
exposed_port = exposed_port.decode().strip()
|
||||
try:
|
||||
addr, port = exposed_port.split(":")
|
||||
__, port = exposed_port.split(":")
|
||||
int(port)
|
||||
except ValueError:
|
||||
log.warning(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ from libervia.backend.core.constants import Const as C
|
|||
from libervia.backend.core.i18n import _
|
||||
from libervia.backend.core.log import getLogger
|
||||
from libervia.backend.memory import persistent
|
||||
from libervia.backend.plugins.plugin_misc_app_manager.models import AppManagerBackend
|
||||
from libervia.backend.plugins.plugin_misc_app_manager.models import (
|
||||
AppManagerBackend,
|
||||
AppUpdateResult,
|
||||
)
|
||||
from libervia.backend.tools.common import data_format
|
||||
from libervia.backend.tools.common import async_process
|
||||
|
||||
|
|
@ -78,12 +81,12 @@ APP_FILE_PREFIX = "libervia_app_"
|
|||
|
||||
|
||||
class AppManager:
|
||||
load = partial(yaml.load, Loader=Loader)
|
||||
dump = partial(yaml.dump, Dumper=Dumper)
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("plugin Applications Manager initialization"))
|
||||
self.host = host
|
||||
self.load = partial(yaml.load, Loader=Loader)
|
||||
self.dump = partial(yaml.dump, Dumper=Dumper)
|
||||
self._managers = {}
|
||||
self._apps = {}
|
||||
self._started = {}
|
||||
|
|
@ -123,6 +126,14 @@ class AppManager:
|
|||
method=self._get_exposed,
|
||||
async_=True,
|
||||
)
|
||||
host.bridge.add_method(
|
||||
"application_update",
|
||||
".plugin",
|
||||
in_sign="s",
|
||||
out_sign="s",
|
||||
method=self._update,
|
||||
async_=True,
|
||||
)
|
||||
# application has been started succeesfully,
|
||||
# args: name, instance_id, extra
|
||||
host.bridge.add_signal("application_started", ".plugin", signature="sss")
|
||||
|
|
@ -558,6 +569,39 @@ class AppManager:
|
|||
|
||||
log.info(f"{app_name!r} stopped")
|
||||
|
||||
def _update(self, app_name):
|
||||
d = defer.ensureDeferred(self.update(str(app_name)))
|
||||
d.addCallback(lambda result: "" if result is None else result.model_dump_json())
|
||||
return d
|
||||
|
||||
async def update(self, app_name: str) -> AppUpdateResult | None:
|
||||
app_name = app_name.lower().strip()
|
||||
app_data = self._started.get(app_name)
|
||||
if app_data:
|
||||
app_data = app_data[0]
|
||||
else:
|
||||
try:
|
||||
app_file_path = next(iter(next(iter(self._apps[app_name].values()))))
|
||||
except KeyError:
|
||||
raise exceptions.NotFound(
|
||||
f"No application found with the name {app_name!r}"
|
||||
)
|
||||
self._app_persistent_data = await self.persistent_data.get(app_name) or {}
|
||||
app_data = self.parse(app_file_path)
|
||||
app_data["_started"] = False
|
||||
app_data["_file_path"] = app_file_path
|
||||
app_data["_name_canonical"] = app_name
|
||||
cache_path = self.host.memory.get_cache_path(
|
||||
PLUGIN_INFO[C.PI_IMPORT_NAME], app_name
|
||||
)
|
||||
app_data["_instance_dir_path"] = cache_path
|
||||
manager = self.get_manager(app_data)
|
||||
log.info(f"updating {app_name!r}")
|
||||
try:
|
||||
return await manager.update(app_data)
|
||||
except Exception as e:
|
||||
raise exceptions.InternalError(f"Can't update {app_name!r}: {e}")
|
||||
|
||||
def _get_exposed(self, identifier, id_type, extra):
|
||||
extra = data_format.deserialise(extra)
|
||||
d = defer.ensureDeferred(self.get_exposed(identifier, id_type, extra))
|
||||
|
|
|
|||
|
|
@ -20,9 +20,27 @@
|
|||
import abc
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from libervia.backend.core.i18n import _
|
||||
|
||||
|
||||
class AppUpdateServiceInfo(BaseModel):
|
||||
"""Info about a service that was updated."""
|
||||
|
||||
service: str = Field(description="Service name")
|
||||
old_version: str = Field(description="Previous version identifier")
|
||||
new_version: str = Field(description="New version identifier")
|
||||
|
||||
|
||||
class AppUpdateResult(BaseModel):
|
||||
"""Result of an app update."""
|
||||
|
||||
updated: list[AppUpdateServiceInfo] = Field(
|
||||
description="List of services that were updated"
|
||||
)
|
||||
|
||||
|
||||
class AppManagerBackend(abc.ABC):
|
||||
"""Abstract class for App Manager."""
|
||||
|
||||
|
|
@ -54,6 +72,15 @@ class AppManagerBackend(abc.ABC):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def update(self, app_data: dict) -> AppUpdateResult | None:
|
||||
"""Update the app.
|
||||
|
||||
@param app_data: A dictionary containing app data.
|
||||
@return: Update result with changed services, or None if no update.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def compute_expose(self, app_data: dict) -> None:
|
||||
"""Compute exposed data for the app.
|
||||
|
|
|
|||
Loading…
Reference in a new issue