Skip to content

Commit

Permalink
NAS-130450 / 24.10 / Add changes to allow executing apps migration mo…
Browse files Browse the repository at this point in the history
…re then once (#14143)

* Add migrated flag to app metadata

* Specify migrated flag in app metadata

* When listing backups, account for installed apps

* Fix syncing catalogs usage

* Handle edge case when migrating apps

* No need to error out if docker dataset already exists

* Only unset pool if some other pool is configured for docker

* Improve running migration response config

* Properly pass on migrated flag
  • Loading branch information
sonicaj authored Aug 7, 2024
1 parent b2bb708 commit 0548b16
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 34 deletions.
6 changes: 4 additions & 2 deletions src/middlewared/middlewared/plugins/apps/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ def do_create(self, job, data):
return self.create_internal(job, app_name, version, data['values'], complete_app_details)

@private
def create_internal(self, job, app_name, version, user_values, complete_app_details, dry_run=False):
def create_internal(
self, job, app_name, version, user_values, complete_app_details, dry_run=False, migrated_app=False,
):
app_version_details = complete_app_details['versions'][version]
self.middleware.call_sync('catalog.version_supported_error_check', app_version_details)

Expand All @@ -190,7 +192,7 @@ def create_internal(self, job, app_name, version, user_values, complete_app_deta
)
new_values = add_context_to_values(app_name, new_values, app_version_details['app_metadata'], install=True)
update_app_config(app_name, version, new_values)
update_app_metadata(app_name, app_version_details)
update_app_metadata(app_name, app_version_details, migrated_app)

job.set_progress(60, 'App installation in progress, pulling images')
if dry_run is False:
Expand Down
4 changes: 3 additions & 1 deletion src/middlewared/middlewared/plugins/apps/ix_apps/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ def get_app_metadata(app_name: str) -> dict[str, typing.Any]:
return {}


def update_app_metadata(app_name: str, app_version_details: dict):
def update_app_metadata(app_name: str, app_version_details: dict, migrated: bool | None = None):
migrated = get_app_metadata(app_name).get('migrated', False) if migrated is None else migrated
with open(get_installed_app_metadata_path(app_name), 'w') as f:
f.write(yaml.safe_dump({
'metadata': app_version_details['app_metadata'],
'migrated': migrated,
**{k: app_version_details[k] for k in ('version', 'human_version')},
**get_portals_and_app_notes(app_name, app_version_details['version']),
}))
Expand Down
2 changes: 0 additions & 2 deletions src/middlewared/middlewared/plugins/catalog/sync.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os

from middlewared.schema import accepts
from middlewared.service import job, private, returns, Service

Expand Down
2 changes: 1 addition & 1 deletion src/middlewared/middlewared/plugins/docker/state_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ async def validate_fs(self):
async def status_change(self):
config = await self.middleware.call('docker.config')
if not config['pool']:
await (await self.middleware.call('catalog.sync')).wait()
return

await self.create_update_docker_datasets(config['dataset'])
await self.middleware.call('catalog.sync')
await self.middleware.call('docker.state.start_service')

@private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ def list_backups(self, job, kubernetes_pool):
apps_mapping = self.middleware.call_sync('catalog.train_to_apps_version_mapping')
catalog_path = self.middleware.call_sync('catalog.config')['location']

docker_config = self.middleware.call_sync('docker.config')
if docker_config['pool'] and docker_config['pool'] != kubernetes_pool:
return backup_config | {
'error': f'Docker pool if configured must be set only to {kubernetes_pool!r} or unset'
}

installed_apps = {}
if docker_config['pool'] == kubernetes_pool:
installed_apps = {app['id']: app for app in self.middleware.call_sync('app.query')}

for snapshot in snapshots:
backup_name = snapshot['name'].split('@', 1)[-1].split(K8s_BACKUP_NAME_PREFIX, 1)[-1]
backup_path = os.path.join(backup_base_dir, backup_name)
Expand All @@ -75,7 +85,7 @@ def list_backups(self, job, kubernetes_pool):
continue

config = release_details(
release.name, release.path, catalog_path, apps_mapping
release.name, release.path, catalog_path, apps_mapping, installed_apps,
)
if config['error']:
backup_data['skipped_releases'].append(config)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def get_default_release_details(release_name: str) -> dict:
}


def release_details(release_name: str, release_path: str, catalog_path: str, apps_mapping: dict) -> dict:
def release_details(
release_name: str, release_path: str, catalog_path: str, apps_mapping: dict, installed_apps: dict,
) -> dict:
config = get_default_release_details(release_name)
if not (release_metadata := get_release_metadata(release_path)) or not all(
k in release_metadata.get('metadata', {}).get('labels', {})
Expand All @@ -50,6 +52,9 @@ def release_details(release_name: str, release_path: str, catalog_path: str, app
if metadata_labels['catalog'] != 'TRUENAS' or metadata_labels['catalog_branch'] != 'master':
return config | {'error': 'Release is not from TrueNAS catalog'}

if release_name in installed_apps:
return config | {'error': 'App with same name is already installed'}

release_train = metadata_labels['catalog_train'] if metadata_labels['catalog_train'] != 'charts' else 'stable'
config['train'] = release_train
if release_train not in apps_mapping:
Expand Down
68 changes: 42 additions & 26 deletions src/middlewared/middlewared/plugins/kubernetes_to_docker/migrate.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import contextlib
import os.path
import shutil

from middlewared.plugins.apps.ix_apps.path import get_app_parent_volume_ds_name, get_installed_app_path
from middlewared.plugins.docker.state_utils import DATASET_DEFAULTS
from middlewared.plugins.docker.utils import applications_ds_name
from middlewared.schema import accepts, Dict, List, returns, Str
from middlewared.service import CallError, InstanceNotFound, job, Service
from middlewared.schema import accepts, Bool, Dict, List, returns, Str
from middlewared.service import CallError, job, Service

from .migrate_config_utils import migrate_chart_release_config

Expand All @@ -30,6 +28,7 @@ class Config:
items=[Dict(
'app_migration_detail',
Str('name'),
Bool('successfully_migrated'),
Str('error', null=True),
)]
))
Expand Down Expand Up @@ -63,28 +62,26 @@ def migrate(self, job, kubernetes_pool, options):
if not backup_config['releases']:
raise CallError(f'No old apps found in {options["backup_name"]!r} backup which can be migrated')

# We will see if docker dataset exists on this pool and if it is there, we will error out
docker_ds = applications_ds_name(kubernetes_pool)
with contextlib.suppress(InstanceNotFound):
self.middleware.call_sync('pool.dataset.get_instance_quick', docker_ds)
raise CallError(f'Docker dataset {docker_ds!r} already exists on {kubernetes_pool!r}')

# For good measure we stop docker service and unset docker pool if any configured
self.middleware.call_sync('service.stop', 'docker')
job.set_progress(15, 'Un-configuring docker service if configured')
docker_job = self.middleware.call_sync('docker.update', {'pool': None})
docker_job.wait_sync()
if docker_job.error:
raise CallError(f'Failed to un-configure docker: {docker_job.error}')

# We will now configure docker service
docker_job = self.middleware.call_sync('docker.update', {'pool': kubernetes_pool})
docker_job.wait_sync()
if docker_job.error:
raise CallError(f'Failed to configure docker: {docker_job.error}')
docker_config = self.middleware.call_sync('docker.config')
if docker_config['pool'] and docker_config['pool'] != kubernetes_pool:
# For good measure we stop docker service and unset docker pool if any configured
self.middleware.call_sync('service.stop', 'docker')
job.set_progress(15, 'Un-configuring docker service if configured')
docker_job = self.middleware.call_sync('docker.update', {'pool': None})
docker_job.wait_sync()
if docker_job.error:
raise CallError(f'Failed to un-configure docker: {docker_job.error}')

if docker_config['pool'] is None or docker_config['pool'] != kubernetes_pool:
# We will now configure docker service
docker_job = self.middleware.call_sync('docker.update', {'pool': kubernetes_pool})
docker_job.wait_sync()
if docker_job.error:
raise CallError(f'Failed to configure docker: {docker_job.error}')

self.middleware.call_sync('catalog.sync').wait_sync()

installed_apps = {app['id']: app for app in self.middleware.call_sync('app.query')}
job.set_progress(25, f'Rolling back to {backup_config["snapshot_name"]!r} snapshot')
self.middleware.call_sync(
'zfs.snapshot.rollback', backup_config['snapshot_name'], {
Expand Down Expand Up @@ -112,8 +109,21 @@ def migrate(self, job, kubernetes_pool, options):
release_config = {
'name': chart_release['release_name'],
'error': 'Unable to complete migration',
'successfully_migrated': False,
}
release_details.append(release_config)

if release_config['name'] in installed_apps:
# Ideally we won't come to this case at all, but this case will only be true in the following case
# User configured docker pool
# Installed X app with same name
# Unset docker pool
# Tried restoring backup on the same pool
# We will run into this case because when we were listing out chart releases which can be migrated
# we were not able to deduce installed apps at all as pool was unset atm and docker wasn't running
release_config['error'] = 'App with same name is already installed'
continue

new_config = migrate_chart_release_config(chart_release | migrate_context)
if isinstance(new_config, str) or not new_config:
release_config['error'] = f'Failed to migrate config: {new_config}'
Expand All @@ -126,7 +136,7 @@ def migrate(self, job, kubernetes_pool, options):
try:
self.middleware.call_sync(
'app.create_internal', dummy_job, chart_release['release_name'],
chart_release['app_version'], new_config, complete_app_details, True,
chart_release['app_version'], new_config, complete_app_details, True, True,
)
except Exception as e:
release_config['error'] = f'Failed to create app: {e}'
Expand Down Expand Up @@ -168,7 +178,10 @@ def migrate(self, job, kubernetes_pool, options):
# We do this to make sure it does not show up as installed in the UI
shutil.rmtree(get_installed_app_path(chart_release['release_name']), ignore_errors=True)
else:
release_config['error'] = None
release_config.update({
'error': None,
'successfully_migrated': True,
})
self.middleware.call_sync('app.metadata.generate').wait_sync(raise_error=True)

job.set_progress(75, 'Deploying migrated apps')
Expand All @@ -184,7 +197,10 @@ def migrate(self, job, kubernetes_pool, options):

for index, status in enumerate(bulk_job.result):
if status['error']:
release_details[index]['error'] = f'Failed to deploy app: {status["error"]}'
release_details[index].update({
'error': f'Failed to deploy app: {status["error"]}',
'successfully_migrated': False,
})

job.set_progress(100, 'Migration completed')

Expand Down

0 comments on commit 0548b16

Please sign in to comment.