-
Notifications
You must be signed in to change notification settings - Fork 494
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NAS-130495 / 24.10 / Add support for deploying custom apps (#14167)
* Rename ix-chart to ix-app * Fix app.available * Add basic custom app svc * Call app.custom.create if custom app creation is desired * Add basic validation for app.custom.create * Expect serialized yaml file for docker compose * Allow specifying both string/dict for custom compose config * Modify setup install app util to account for custom app * Amend updating config func to account for custom apps * Amend metadata func to account for custom app * Complete custom app create implementation: * Add validation util for custom compose app * Get updates to work for custom apps * Fix upgrade available flag for custom apps * Add internal endpoint for deleting app * Add util to get rendered docker compose config * Add ability to convert any app to advanced app * Add public endpoint to convert any app to custom app * Fix minor bug * Fix max length
- Loading branch information
Showing
10 changed files
with
220 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import contextlib | ||
import shutil | ||
|
||
from catalog_reader.custom_app import get_version_details | ||
|
||
from middlewared.service import CallError, Service | ||
|
||
from .compose_utils import compose_action | ||
from .custom_app_utils import validate_payload | ||
from .ix_apps.lifecycle import get_rendered_template_config_of_app, update_app_config | ||
from .ix_apps.metadata import update_app_metadata | ||
from .ix_apps.path import get_installed_app_path | ||
from .ix_apps.setup import setup_install_app_dir | ||
|
||
|
||
class AppCustomService(Service): | ||
|
||
class Config: | ||
namespace = 'app.custom' | ||
private = True | ||
|
||
def convert(self, job, app_name): | ||
app = self.middleware.call_sync('app.get_instance', app_name) | ||
if app['custom_app'] is True: | ||
raise CallError(f'{app_name!r} is already a custom app') | ||
|
||
rendered_config = get_rendered_template_config_of_app(app_name, app['version']) | ||
if not rendered_config: | ||
raise CallError(f'No rendered config found for {app_name!r}') | ||
|
||
job.set_progress(10, 'Completed initial validation for conversion of app to custom app') | ||
# What needs to happen here is the following: | ||
# Merge all available compose files into one of the app and hold on to it | ||
# Do an uninstall of the app and create it again with the new compose file | ||
# Update metadata to reflect that this is a custom app | ||
# Finally update collective metadata | ||
job.set_progress(20, 'Removing existing app\'s docker resources') | ||
self.middleware.call_sync( | ||
'app.delete_internal', type('dummy_job', (object,), {'set_progress': lambda *args: None})(), | ||
app_name, app, {'remove_images': False, 'remove_ix_volumes': False} | ||
) | ||
|
||
return self.create({ | ||
'app_name': app_name, | ||
'custom_compose_config': rendered_config, | ||
}, job) | ||
|
||
def create(self, data, job=None, progress_base=0): | ||
""" | ||
Create a custom app. | ||
""" | ||
compose_config = validate_payload(data, 'app_create') | ||
|
||
def update_progress(percentage_done, message): | ||
job.set_progress(int((100 - progress_base) * (percentage_done / 100)) + progress_base, message) | ||
|
||
# For debug purposes | ||
job = job or type('dummy_job', (object,), {'set_progress': lambda *args: None})() | ||
update_progress(25, 'Initial validation completed for custom app creation') | ||
|
||
app_name = data['app_name'] | ||
app_version_details = get_version_details() | ||
version = app_version_details['version'] | ||
try: | ||
update_progress(35, 'Setting up App directory') | ||
setup_install_app_dir(app_name, app_version_details, custom_app=True) | ||
update_app_config(app_name, version, compose_config, custom_app=True) | ||
update_app_metadata(app_name, app_version_details, migrated=False, custom_app=True) | ||
|
||
update_progress(60, 'App installation in progress, pulling images') | ||
compose_action(app_name, version, 'up', force_recreate=True, remove_orphans=True) | ||
except Exception as e: | ||
update_progress(80, f'Failure occurred while installing {app_name!r}, cleaning up') | ||
for method, args, kwargs in ( | ||
(compose_action, (app_name, version, 'down'), {'remove_orphans': True}), | ||
(shutil.rmtree, (get_installed_app_path(app_name),), {}), | ||
): | ||
with contextlib.suppress(Exception): | ||
method(*args, **kwargs) | ||
|
||
raise e from None | ||
else: | ||
self.middleware.call_sync('app.metadata.generate').wait_sync(raise_error=True) | ||
job.set_progress(100, f'{app_name!r} installed successfully') | ||
return self.middleware.call_sync('app.get_instance', app_name) |
23 changes: 23 additions & 0 deletions
23
src/middlewared/middlewared/plugins/apps/custom_app_utils.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import yaml | ||
|
||
from middlewared.service import ValidationErrors | ||
|
||
|
||
def validate_payload(data: dict, schema: str) -> dict: | ||
verrors = ValidationErrors() | ||
compose_keys = ('custom_compose_config', 'custom_compose_config_string') | ||
if all(not data.get(k) for k in compose_keys): | ||
verrors.add(f'{schema}.custom_compose_config', 'This field is required') | ||
elif all(data.get(k) for k in compose_keys): | ||
verrors.add(f'{schema}.custom_compose_config_string', 'Only one of these fields should be provided') | ||
|
||
compose_config = data.get('custom_compose_config') | ||
if data.get('custom_compose_config_string'): | ||
try: | ||
compose_config = yaml.safe_load(data['custom_compose_config_string']) | ||
except yaml.YAMLError: | ||
verrors.add('app_create.custom_compose_config_string', 'Invalid YAML provided') | ||
|
||
verrors.check() | ||
|
||
return compose_config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 2 additions & 11 deletions
13
src/middlewared/middlewared/plugins/apps/ix_apps/portals.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,29 @@ | ||
import os | ||
import shutil | ||
import textwrap | ||
import yaml | ||
|
||
from .metadata import update_app_yaml_for_last_update | ||
from .path import get_app_parent_config_path, get_installed_app_version_path | ||
|
||
|
||
def setup_install_app_dir(app_name: str, app_version_details: dict): | ||
def setup_install_app_dir(app_name: str, app_version_details: dict, custom_app: bool = False): | ||
os.makedirs(os.path.join(get_app_parent_config_path(), app_name, 'versions'), exist_ok=True) | ||
to_install_app_version = os.path.basename(app_version_details['version']) | ||
destination = get_installed_app_version_path(app_name, to_install_app_version) | ||
shutil.copytree(app_version_details['location'], destination) | ||
if custom_app: | ||
# TODO: See if it makes sense to creat a dummy app on apps side instead | ||
os.makedirs(os.path.join(destination, 'templates/rendered'), exist_ok=True) | ||
with open(os.path.join(destination, 'README.md'), 'w') as f: | ||
f.write(textwrap.dedent(''' | ||
# Custom App | ||
This is a custom app where user can use his/her own docker compose file for deploying services. | ||
''')) | ||
|
||
with open(os.path.join(destination, 'app.yaml'), 'w') as f: | ||
f.write(yaml.safe_dump(app_version_details['app_metadata'])) | ||
else: | ||
shutil.copytree(app_version_details['location'], destination) | ||
|
||
update_app_yaml_for_last_update(destination, app_version_details['last_update']) |
Oops, something went wrong.