diff --git a/buildchain/buildchain/iso.py b/buildchain/buildchain/iso.py index d7f25afb59..6c483e736e 100644 --- a/buildchain/buildchain/iso.py +++ b/buildchain/buildchain/iso.py @@ -64,7 +64,7 @@ files=( Path('common.sh'), Path('iso-manager.sh'), - Path('solution-manager.sh'), + Path('solutions.sh'), helper.TemplateFile( task_name='downgrade.sh', source=constants.ROOT/'scripts'/'downgrade.sh.in', diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 0e8c172db2..2b3acfb122 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -235,8 +235,9 @@ def _get_parts(self) -> Iterator[str]: Path('salt/metalk8s/addons/ui/deployed/namespace.sls'), Path('salt/metalk8s/addons/ui/deployed/ui.sls'), - Path('salt/metalk8s/addons/solutions/deployed.sls'), - Path('salt/metalk8s/addons/solutions/environment-crd.sls'), + Path('salt/metalk8s/addons/solutions/deployed/configmap.sls'), + Path('salt/metalk8s/addons/solutions/deployed/init.sls'), + Path('salt/metalk8s/addons/solutions/deployed/namespace.sls'), Path('salt/metalk8s/addons/volumes/deployed.sls'), targets.TemplateFile( @@ -413,30 +414,37 @@ def _get_parts(self) -> Iterator[str]: Path('salt/metalk8s/node/grains.sls'), + Path('salt/metalk8s/orchestrate/deploy_node.sls'), + Path('salt/metalk8s/orchestrate/etcd.sls'), + Path('salt/metalk8s/orchestrate/register_etcd.sls'), + Path('salt/metalk8s/orchestrate/bootstrap/init.sls'), Path('salt/metalk8s/orchestrate/bootstrap/accept-minion.sls'), - Path('salt/metalk8s/orchestrate/deploy_node.sls'), + Path('salt/metalk8s/orchestrate/downgrade/init.sls'), Path('salt/metalk8s/orchestrate/downgrade/precheck.sls'), Path('salt/metalk8s/orchestrate/downgrade/pre.sls'), - Path('salt/metalk8s/orchestrate/solutions/available.sls'), - Path('salt/metalk8s/orchestrate/solutions/init.sls'), - Path('salt/metalk8s/orchestrate/etcd.sls'), + Path('salt/metalk8s/orchestrate/upgrade/init.sls'), Path('salt/metalk8s/orchestrate/upgrade/precheck.sls'), Path('salt/metalk8s/orchestrate/upgrade/pre.sls'), - Path('salt/metalk8s/orchestrate/register_etcd.sls'), + + Path('salt/metalk8s/orchestrate/solutions/prepare-environment.sls'), + Path('salt/metalk8s/orchestrate/solutions/deploy-components.sls'), + Path('salt/metalk8s/orchestrate/solutions/files/operator/configmap.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/operator/deployment.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/operator/role.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/operator/role_binding.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/operator/service_account.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/ui/configmap.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/ui/deployment.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/ui/ingress.yaml'), + Path('salt/metalk8s/orchestrate/solutions/files/ui/service.yaml'), Path('salt/metalk8s/archives/configured.sls'), Path('salt/metalk8s/archives/init.sls'), Path('salt/metalk8s/archives/mounted.sls'), - Path('salt/metalk8s/solutions/configured.sls'), - Path('salt/metalk8s/solutions/mounted.sls'), - Path('salt/metalk8s/solutions/unconfigured.sls'), - Path('salt/metalk8s/solutions/unmounted.sls'), - Path('salt/metalk8s/solutions/init.sls'), - Path('salt/metalk8s/repo/configured.sls'), Path('salt/metalk8s/repo/deployed.sls'), Path('salt/metalk8s/repo/files/apt.sources.list.j2'), @@ -481,6 +489,9 @@ def _get_parts(self) -> Iterator[str]: Path('salt/metalk8s/salt/minion/local.sls'), Path('salt/metalk8s/salt/minion/running.sls'), + Path('salt/metalk8s/solutions/available.sls'), + Path('salt/metalk8s/solutions/init.sls'), + Path('salt/metalk8s/volumes/init.sls'), Path('salt/metalk8s/volumes/prepared/init.sls'), Path('salt/metalk8s/volumes/prepared/installed.sls'), @@ -491,26 +502,26 @@ def _get_parts(self) -> Iterator[str]: Path('salt/_modules/containerd.py'), Path('salt/_modules/cri.py'), + Path('salt/_modules/metalk8s.py'), Path('salt/_modules/metalk8s_cordon.py'), Path('salt/_modules/metalk8s_drain.py'), - Path('salt/_modules/metalk8s_kubernetes.py'), Path('salt/_modules/metalk8s_etcd.py'), Path('salt/_modules/metalk8s_grafana.py'), + Path('salt/_modules/metalk8s_kubernetes.py'), Path('salt/_modules/metalk8s_kubernetes_utils.py'), - Path('salt/_modules/metalk8s.py'), Path('salt/_modules/metalk8s_network.py'), Path('salt/_modules/metalk8s_package_manager_yum.py'), Path('salt/_modules/metalk8s_package_manager_apt.py'), - Path('salt/_modules/metalk8s_volumes.py'), Path('salt/_modules/metalk8s_solutions.py'), - + Path('salt/_modules/metalk8s_solutions_k8s.py'), + Path('salt/_modules/metalk8s_volumes.py'), Path('salt/_pillar/metalk8s.py'), Path('salt/_pillar/metalk8s_endpoints.py'), + Path('salt/_pillar/metalk8s_etcd.py'), Path('salt/_pillar/metalk8s_nodes.py'), Path('salt/_pillar/metalk8s_private.py'), Path('salt/_pillar/metalk8s_solutions.py'), - Path('salt/_pillar/metalk8s_etcd.py'), Path('salt/_renderers/metalk8s_kubernetes.py'), diff --git a/docs/developer/solutions/deploy.rst b/docs/developer/solutions/deploy.rst index 75ef269a97..66dfa7f8c5 100644 --- a/docs/developer/solutions/deploy.rst +++ b/docs/developer/solutions/deploy.rst @@ -2,24 +2,137 @@ Deploying And Experimenting ============================ Given the solution ISO is correctly generated, a script utiliy has been -added to enable solution install and removal +added to manage Solutions. +This script is located at the root of Metalk8s archive: -Installation ------------- + .. code:: + + /srv/scality/metalk8s-X.X.X/solutions.sh + +Import a Solution +----------------- + +Importing a Solution will mount its ISO and expose its container images. + +To import a Solution into MetalK8s cluster, use the ``import`` subcommand: + + .. code:: + + ./solutions.sh import --archive + +The ``--archive`` option can be provided multiple times to import several +Solutions ISOs at the same time: + + .. code:: + + ./solutions.sh import --archive \ + --archive + +Unimport a Solution +------------------- + +To unimport a Solution from MetalK8s cluster, use the ``unimport`` subcommand: + + .. warning:: + + Images of a Solution will no longer be available after an archive removal + + .. code:: + + ./solutions.sh unimport --archive + +Activate a Solution +------------------- + +Activating a Solution version will deploy its CRDs. + +To activate a Solution in MetalK8s cluster, use the ``activate`` subcommand: + + .. code:: + + ./solutions.sh activate --name --version + +Deactivate a Solution +--------------------- + +To deactivate a Solution from Metalk8s cluster, use the ``deactivate`` +subcommand: + + .. code:: + + ./solutions.sh deactivate --name + +Create an Environment +--------------------- + +To create a Solution Environment, use the ``create-env`` subcommand: + + .. code:: + + ./solutions.sh create-env --name + +By default, it will create a Namespace named after the ````, +but it can be changed, using the ``--namespace`` option: + + .. code:: + + ./solutions.sh create-env --name \ + --namespace + +It's also possible to use the previous command to create multiple Namespaces +(one at a time) in this Environment, allowing Solutions to run in different +Namespaces. + +Delete an Environment +--------------------- + +To delete an Environment, use the ``delete-env`` subcommand: -Use the `solution-manager.sh` script to install a new solution ISO using -the following command + .. warning:: + + This will destroy everything in the said Environment, with no way back .. code:: - /src/scality/metalk8s-X.X.X/solution-manager.sh -a/--add + ./solutions.sh delete-env --name + +In case of multiple Namespaces inside an Environment, it's also possible +to only delete a single Namespace, using: + + .. code:: + + ./solutions.sh delete-env --name \ + --namespace + +Add a Solution in an Environment +-------------------------------- + +Adding a Solution will deploy its UI and Operator resources in the Environment. -Removal -------- +To add a Solution in an Environment, use the ``add-solution`` subcommand: -To remove a solution from the cluster use the previous script by invoking + .. code:: + + ./solutions.sh add-solution --name \ + --solution --version + +In case of non-default Namespace (not corresponding to ````) +or multiple Namespaces in an Environment, Namespace in which the Solution will +be added must be precised, using the ``--namespace`` option: .. code:: - /src/scality/metalk8s-X.X.X/solution-manager.sh -d/--del + ./solutions.sh add-solution --name \ + --solution --version \ + --namespace + +Delete a Solution from an Environment +------------------------------------- + +To delete a Solution from an Environment, use the ``delete-solution`` +subcommand: + + .. code:: + ./solutions.sh delete-solution --name \ + --solution diff --git a/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index 13c8f565b8..dadc2a15ba 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -1,186 +1,258 @@ -''' -Various utilities to manage Solutions. -''' -import json +"""Utility methods for Solutions management. + +This module contains minion-local operations, see `metalk8s_solutions_k8s.py` +for the K8s operations in the virtual `metalk8s_solutions` module. +""" +import collections +import errno +import os import logging import yaml +import salt from salt.exceptions import CommandExecutionError -HAS_LIBS = True -SOLUTIONS_CONFIG_MAP = 'metalk8s-solutions' -SOLUTIONS_CONFIG_MAP_NAMESPACE = 'metalk8s-solutions' -SOLUTIONS_CONFIG_FILE = '/etc/metalk8s/solutions.yaml' -try: - import kubernetes.client - from kubernetes.client.rest import ApiException - from urllib3.exceptions import HTTPError -except ImportError: - HAS_LIBS = False - log = logging.getLogger(__name__) +CONFIG_FILE = '/etc/metalk8s/solutions.yaml' +CONFIG_KIND = 'SolutionsConfiguration' +SUPPORTED_CONFIG_VERSIONS = [ + 'solutions.metalk8s.scality.com/{}'.format(version) + for version in ['v1alpha1'] +] __virtualname__ = 'metalk8s_solutions' def __virtual__(): - if HAS_LIBS: - return __virtualname__ - else: - return False, 'python kubernetes library not found' - - -def list_deployed( - context="kubernetes-admin@kubernetes", - kubeconfig="/etc/kubernetes/admin.conf" -): - """Get all deployed Solution versions from a known ConfigMap.""" - response_dict = __salt__['metalk8s_kubernetes.show_configmap']( - SOLUTIONS_CONFIG_MAP, - namespace=SOLUTIONS_CONFIG_MAP_NAMESPACE, - context=context, - kubeconfig=kubeconfig, - ) - if not response_dict or not response_dict.get('data'): - return {} + if 'metalk8s.archive_info_from_iso' not in __salt__: + return False, "Failed to load 'metalk8s' module." + return __virtualname__ - return { - name: json.loads(versions_str) - for name, versions_str in response_dict['data'].items() - } + +def _load_config_file(create=False): + try: + with open(CONFIG_FILE, 'r') as fd: + return yaml.safe_load(fd) + except IOError as exc: + if create and exc.errno == errno.ENOENT: + return _create_config_file() + msg = 'Failed to load "{}": {}'.format(CONFIG_FILE, str(exc)) + raise CommandExecutionError(message=msg) -def list_configured(): - """Get list of Solution archives paths defined in a config file.""" +def _write_config_file(data): try: - with open(SOLUTIONS_CONFIG_FILE, 'r') as fd: - content = yaml.safe_load(fd) + with open(CONFIG_FILE, 'w') as fd: + yaml.safe_dump(data, fd) except Exception as exc: - msg = 'Failed to load "{}": {}'.format(SOLUTIONS_CONFIG_FILE, str(exc)) + msg = 'Failed to write Solutions config file at "{}": {}'.format( + CONFIG_FILE, exc + ) raise CommandExecutionError(message=msg) - return content.get('archives', []) or [] - - -def register_solution_version( - name, - version, - archive_path, - deployed=False, - context="kubernetes-admin@kubernetes", - kubeconfig="/etc/kubernetes/admin.conf" -): - """Add a Solution version to the ConfigMap.""" - cfg = __salt__['metalk8s_kubernetes.setup_conn']( - context=context, - kubeconfig=kubeconfig - ) - api_instance = kubernetes.client.CoreV1Api() - - # Retrieve the existing ConfigMap - configmap = __salt__['metalk8s_kubernetes.show_configmap']( - SOLUTIONS_CONFIG_MAP, - namespace=SOLUTIONS_CONFIG_MAP_NAMESPACE, - context=context, - kubeconfig=kubeconfig, - ) - if configmap is None: - configmap = __salt__['metalk8s_kubernetes.create_configmap']( - SOLUTIONS_CONFIG_MAP, - namespace=SOLUTIONS_CONFIG_MAP_NAMESPACE, - data=None, - context=context, - kubeconfig=kubeconfig, + +def _create_config_file(): + default_data = { + 'apiVersion': SUPPORTED_CONFIG_VERSIONS[0], + 'kind': CONFIG_KIND, + 'archives': [], + 'active': {}, + } + _write_config_file(default_data) + return default_data + + +def read_config(create=False): + """Read the SolutionsConfiguration file and return its contents. + + Empty containers will be used for `archives` and `active` in the return + value. + + The format should look like the following example: + + ..code-block:: yaml + + apiVersion: metalk8s.scality.com/v1alpha1 + kind: SolutionsConfiguration + archives: + - /path/to/solution/archive.iso + active: + solution-name: X.Y.Z-suffix (or 'latest') + + If `create` is set to True, this will create an empty configuration file + if it does not exist yet. + """ + config = _load_config_file(create=create) + + if config.get('kind') != 'SolutionsConfiguration': + raise CommandExecutionError( + 'Invalid `kind` in configuration ({}), ' + 'must be "SolutionsConfiguration"'.format(config.get('kind')) ) - all_versions_str = (configmap.get('data') or {}).get(name, '[]') - all_versions = json.loads(all_versions_str) + if config.get('apiVersion') not in SUPPORTED_CONFIG_VERSIONS: + raise CommandExecutionError( + 'Invalid `apiVersion` in configuration ({}), ' + 'must be one of: {}'.format( + config.get('apiVersion'), + ', '.join(SUPPORTED_CONFIG_VERSIONS) + ) + ) + + config.setdefault('archives', []) + config.setdefault('active', {}) + + return config + - for version_dict in all_versions: - if version_dict['version'] == version: - version_dict['iso'] = archive_path - version_dict['deployed'] = deployed - break +def configure_archive(archive, create_config=False, removed=False): + """Add (or remove) a Solution archive in the config file.""" + config = read_config(create=create_config) + + if removed: + try: + config['archives'].remove(archive) + except ValueError: + pass else: - all_versions.append({ - 'version': version, - 'iso': archive_path, - 'deployed': deployed, - }) + if archive not in config['archives']: + config['archives'].append(archive) - body = {'data': {name: json.dumps(all_versions)}} + _write_config_file(config) + return True - try: - api_instance.patch_namespaced_config_map( - SOLUTIONS_CONFIG_MAP, - SOLUTIONS_CONFIG_MAP_NAMESPACE, - body + +def activate_solution(solution, version='latest'): + """Set a `version` of a `solution` as being "active".""" + available = list_available() + if solution not in available: + raise CommandExecutionError( + 'Cannot activate Solution "{}": not available'.format(solution) ) - except ApiException as exc: - log.exception('Failed to patch ConfigMap "%s": %s', - SOLUTIONS_CONFIG_MAP, exc) - return False + # NOTE: this doesn't create a config file, since you can't activate a non- + # available version + config = read_config(create=False) + + if version != 'latest': + if version not in (info['version'] for info in available[solution]): + raise CommandExecutionError( + 'Cannot activate version "{}" for Solution "{}": ' + 'not available'.format(version, solution) + ) + + config['active'][solution] = version + _write_config_file(config) return True -def unregister_solution_version( - name, - version, - context="kubernetes-admin@kubernetes", - kubeconfig="/etc/kubernetes/admin.conf" -): - """Remove a Solution version from the ConfigMap.""" - cfg = __salt__['metalk8s_kubernetes.setup_conn']( - context=context, - kubeconfig=kubeconfig - ) - api_instance = kubernetes.client.CoreV1Api() - - # Retrieve the existing ConfigMap - configmap = __salt__['metalk8s_kubernetes.show_configmap']( - SOLUTIONS_CONFIG_MAP, - namespace=SOLUTIONS_CONFIG_MAP_NAMESPACE, - context=context, - kubeconfig=kubeconfig, - ) - if not configmap: - log.exception( - 'Cannot unregister Solution: ConfigMap "%s" is missing.', - SOLUTIONS_CONFIG_MAP - ) + +def deactivate_solution(solution): + """Remove a `solution` from the "active" section in configuration.""" + config = read_config(create=False) + config['active'].pop(solution, None) + _write_config_file(config) + return True + + +def _is_solution_mount(mount_tuple): + """Return whether a mount is for a Solution archive. + + Any ISO9660 mounted in `/srv/scality` that isn't for MetalK8s is considered + to be a Solution archive. + """ + mountpoint, mount_info = mount_tuple + + if not mountpoint.startswith('/srv/scality/'): return False - old_versions = json.loads((configmap['data'] or {}).get(name, '[]')) - new_versions = [ - version_dict for version_dict in old_versions - if version_dict['version'] != version - ] - # If this is the last registered version then remove all the entry - if new_versions == []: - del configmap['data'][name] - # Patching a CM while removing a key does not work, we need to replace it - try: - api_instance.replace_namespaced_config_map( - SOLUTIONS_CONFIG_MAP, - SOLUTIONS_CONFIG_MAP_NAMESPACE, - configmap - ) - except ApiException as exc: - log.exception('Failed to patch ConfigMap "%s": %s', - SOLUTIONS_CONFIG_MAP, str(exc)) - return False - else: - body = {'data': {name: json.dumps(new_versions)}} - try: - api_instance.patch_namespaced_config_map( - SOLUTIONS_CONFIG_MAP, - SOLUTIONS_CONFIG_MAP_NAMESPACE, - body - ) - except ApiException as exc: - log.exception('Failed to patch ConfigMap "%s": %s', - SOLUTIONS_CONFIG_MAP, str(exc)) - return False + if mountpoint.startswith('/srv/scality/metalk8s-'): + return False + + if mount_info['fstype'] != 'iso9660': + return False return True + + +SOLUTION_CONFIG_KIND = 'SolutionConfig' +SOLUTION_CONFIG_APIVERSIONS = [ + 'solutions.metalk8s.scality.com/v1alpha1', +] + + +def _default_solution_config(name, version): + return { + 'kind': SOLUTION_CONFIG_KIND, + 'apiVersion': SOLUTION_CONFIG_APIVERSIONS[0], + 'operator': { + 'image': { + 'name': '{}-operator'.format(name), + 'tag': version, + }, + }, + 'ui': { + 'image': { + 'name': '{}-ui'.format(name), + 'tag': version, + }, + }, + 'customApiGroups': [], + } + + +def read_solution_config(mountpoint, name, version): + log.debug('Reading Solution config from %r', mountpoint) + config = _default_solution_config(name, version) + config_path = os.path.join(mountpoint, 'config.yaml') + + if not os.path.isfile(config_path): + log.debug('Solution mounted at %r has no "config.yaml"', mountpoint) + return config + + with salt.utils.files.fopen(config_path, 'r') as stream: + provided_config = salt.utils.yaml.safe_load(stream) + + provided_kind = provided_config.pop('kind', None) + provided_api_version = provided_config.pop('apiVersion', None) + + if ( + provided_kind != SOLUTION_CONFIG_KIND or + provided_api_version not in SOLUTION_CONFIG_APIVERSIONS + ): + raise CommandExecutionError( + 'Wrong apiVersion/kind for {}'.format(config_path) + ) + + salt.utils.dictupdate.update(config, provided_config) + return config + + +def list_available(): + """Get a view of mounted Solution archives. + + Result is in the shape of a dict, with Solution names as keys, and lists + of mounted archives (each being a dict of various info) as values. + """ + result = collections.defaultdict(list) + + active_mounts = __salt__['mount.active']() + + solution_mounts = filter(_is_solution_mount, active_mounts.items()) + + for mountpoint, mount_info in solution_mounts: + solution_info = __salt__['metalk8s.archive_info_from_tree'](mountpoint) + name = solution_info['name'] + machine_name = name.replace(' ', '-').lower() + version = solution_info['version'] + + result[machine_name].append({ + 'name': name, + 'id': '{}-{}'.format(machine_name, version), + 'mountpoint': mountpoint, + 'archive': mount_info['alt_device'], + 'version': version, + 'config': read_solution_config(mountpoint, machine_name, version), + }) + + return dict(result) diff --git a/salt/_modules/metalk8s_solutions_k8s.py b/salt/_modules/metalk8s_solutions_k8s.py new file mode 100644 index 0000000000..b27478d58b --- /dev/null +++ b/salt/_modules/metalk8s_solutions_k8s.py @@ -0,0 +1,97 @@ +"""Utility methods for Solutions management. + +This module contains only K8s operations, see `metalk8s_solutions.py` for the +for the rest of the operations in the virtual `metalk8s_solutions` module. +""" +import json +import logging + +from salt.exceptions import CommandExecutionError + +log = logging.getLogger(__name__) + +__virtualname__ = 'metalk8s_solutions' + +SOLUTIONS_NAMESPACE = 'metalk8s-solutions' +SOLUTIONS_CONFIGMAP_NAME = 'metalk8s-solutions' +ENVIRONMENT_LABEL = 'solutions.metalk8s.scality.com/environment' +ENVIRONMENT_DESCRIPTION_ANNOTATION = \ + 'solutions.metalk8s.scality.com/environment-description' +ENVIRONMENT_CONFIGMAP_NAME = 'metalk8s-environment' + + +def __virtual__(): + # TODO: consider checking methods from metalk8s_kubernetes + return __virtualname__ + + +def list_active(**kwargs): + """List all Solution versions for which components are deployed. + + Currently relies on the ConfigMap that is managed in the + `deploy-components` orchestration. + """ + solutions_config = __salt__['metalk8s_kubernetes.get_object']( + kind='ConfigMap', + apiVersion='v1', + name=SOLUTIONS_CONFIGMAP_NAME, + namespace=SOLUTIONS_NAMESPACE, + **kwargs + ) + result = {} + + if solutions_config is None: + return result + + for name, versions_str in (solutions_config.get('data') or {}).items(): + versions = json.loads(versions_str) + active_version = next( + (version['version'] for version in versions if version['active']), + None + ) + if active_version is not None: + result[name] = active_version + + return result + + +def list_environments(**kwargs): + """List all Environments (through labelled namespaces) and their config. + + Each Environment can be made up of multiple namespaces, though only one + ConfigMap will be used to determine the Environment configuration (the + first one found will be selected). + """ + env_namespaces = __salt__['metalk8s_kubernetes.list_objects']( + kind='Namespace', + apiVersion='v1', + label_selector=ENVIRONMENT_LABEL, # just testing existence, not value + **kwargs + ) + + environments = {} + for namespace in env_namespaces: + name = namespace['metadata']['labels'][ENVIRONMENT_LABEL] + env = environments.setdefault(name, {'name': name}) + + description = (namespace['metadata'].get('annotations') or {}).get( + ENVIRONMENT_DESCRIPTION_ANNOTATION + ) + if description is not None and 'description' not in env: + env['description'] = description + + namespaces = env.setdefault('namespaces', {}) + + config = __salt__['metalk8s_kubernetes.get_object']( + kind='ConfigMap', + apiVersion='v1', + name=ENVIRONMENT_CONFIGMAP_NAME, + namespace=namespace['metadata']['name'], + **kwargs + ) or {} + + namespaces[namespace['metadata']['name']] = { + 'config': config.get('data') + } + + return environments diff --git a/salt/_pillar/metalk8s_solutions.py b/salt/_pillar/metalk8s_solutions.py index 8ebb1b15de..a96d45cce9 100644 --- a/salt/_pillar/metalk8s_solutions.py +++ b/salt/_pillar/metalk8s_solutions.py @@ -7,44 +7,94 @@ def __virtual__(): + if 'metalk8s_solutions.read_config' not in __salt__: + return False, "Failed to load 'metalk8s_solution' module." return __virtualname__ -def _load_solutions(): +def _load_solutions(bootstrap_id): """Load Solutions from ConfigMap and config file.""" - errors = [] + result = { + 'available': {}, + 'config': {}, + 'environments': {}, + } + try: - deployed = __salt__['metalk8s_solutions.list_deployed']() - except KeyError: - return __utils__['pillar_utils.errors_to_dict']([ - "Failed to load 'metalk8s_solutions' module." + result['config'] = __salt__['metalk8s_solutions.read_config']() + except (IOError, CommandExecutionError) as exc: + result['config'] = __utils__['pillar_utils.errors_to_dict']([ + "Error when reading Solutions config file: {}".format(exc) ]) + + errors = [] + try: + result['available'] = __salt__['saltutil.cmd']( + tgt=bootstrap_id, + fun='metalk8s_solutions.list_available', + )[bootstrap_id]['ret'] except Exception as exc: - deployed = {} errors.append( - "Error when retrieving ConfigMap 'metalk8s-solutions': {}".format( - exc - ) + "Error when listing available Solutions: {}".format(exc) ) try: - configured = __salt__['metalk8s_solutions.list_configured']() - except (IOError, CommandExecutionError) as exc: - configured = [] + active = __salt__['metalk8s_solutions.list_active']() + except Exception as exc: errors.append( - "Error when reading Solutions config file: {}".format(exc) + "Error when listing active Solution versions: {}".format(exc) ) - result = { - 'configured': configured, - 'deployed': deployed, - } - if errors: - result.update(__utils__['pillar_utils.errors_to_dict'](errors)) + result['available'].update( + __utils__['pillar_utils.errors_to_dict'](errors) + ) + else: + # Set `active` flag on active Solution versions + for solution, versions in result['available'].items(): + active_version = active.get(solution) + for version_info in versions: + version_info['active'] = \ + version_info['version'] == active_version + + try: + result['environments'] = \ + __salt__['metalk8s_solutions.list_environments']() + except Exception as exc: + result['environments'] = __utils__['pillar_utils.errors_to_dict']([ + "Error when listing Solution Environments: {}".format(exc) + ]) + + for key in ['available', 'config', 'environments']: + __utils__['pillar_utils.promote_errors'](result, key) return result def ext_pillar(minion_id, pillar): - return {"metalk8s": {'solutions': _load_solutions()}} + # NOTE: this ext_pillar relies on the `metalk8s_nodes` ext_pillar to find + # the Bootstrap minion ID, for the remote execution of + # `metalk8s_solutions.list_available`. + errors = [] + pillar_nodes = pillar.get('metalk8s', {}).get('nodes', {}) + if '_errors' in pillar_nodes: + errors.append("Pillar 'metalk8s:nodes' has errors") + else: + bootstrap_nodes = [ + node_name for node_name, node_info in pillar_nodes.items() + if 'bootstrap' in node_info['roles'] + ] + try: + bootstrap_id, = bootstrap_nodes + except ValueError: + errors.append( + 'Must have one and only one bootstrap Node (found {})'.format( + len(bootstrap_nodes) + ) + ) + + if errors: + error_dict = __utils__['pillar_utils.errors_to_dict'](errors) + return {"metalk8s": {"solutions": error_dict}} + + return {"metalk8s": {'solutions': _load_solutions(bootstrap_id)}} diff --git a/salt/metalk8s/addons/solutions/deployed.sls b/salt/metalk8s/addons/solutions/deployed.sls deleted file mode 100644 index 2be6108948..0000000000 --- a/salt/metalk8s/addons/solutions/deployed.sls +++ /dev/null @@ -1,2 +0,0 @@ -include: - - .environment-crd diff --git a/salt/metalk8s/addons/solutions/deployed/configmap.sls b/salt/metalk8s/addons/solutions/deployed/configmap.sls new file mode 100644 index 0000000000..2c327db4d2 --- /dev/null +++ b/salt/metalk8s/addons/solutions/deployed/configmap.sls @@ -0,0 +1,7 @@ +#!metalk8s_kubernetes + +apiVersion: v1 +kind: ConfigMap +metadata: + name: metalk8s-solutions + namespace: metalk8s-solutions diff --git a/salt/metalk8s/addons/solutions/deployed/init.sls b/salt/metalk8s/addons/solutions/deployed/init.sls new file mode 100644 index 0000000000..64bc08233a --- /dev/null +++ b/salt/metalk8s/addons/solutions/deployed/init.sls @@ -0,0 +1,3 @@ +include: + - .namespace + - .configmap diff --git a/salt/metalk8s/addons/solutions/deployed/namespace.sls b/salt/metalk8s/addons/solutions/deployed/namespace.sls new file mode 100644 index 0000000000..0d9c5d94e0 --- /dev/null +++ b/salt/metalk8s/addons/solutions/deployed/namespace.sls @@ -0,0 +1,10 @@ +#!metalk8s_kubernetes + +apiVersion: v1 +kind: Namespace +metadata: + name: metalk8s-solutions + labels: + app.kubernetes.io/managed-by: metalk8s + app.kubernetes.io/part-of: metalk8s + heritage: metalk8s diff --git a/salt/metalk8s/addons/solutions/environment-crd.sls b/salt/metalk8s/addons/solutions/environment-crd.sls deleted file mode 100644 index ac393dd871..0000000000 --- a/salt/metalk8s/addons/solutions/environment-crd.sls +++ /dev/null @@ -1,66 +0,0 @@ -#! metalk8s_kubernetes kubeconfig=/etc/kubernetes/admin.conf&context=kubernetes-admin@kubernetes - -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: environments.solutions.metalk8s.scality.com -spec: - group: solutions.metalk8s.scality.com - names: - kind: Environment - listKind: EnvironmentList - plural: environments - singular: environment - scope: Cluster - subresources: - status: {} - validation: - openAPIV3Schema: - description: 'An Environment represents a collection of Namespaces in which - are deployed Solution instances.' - type: object - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this - representation of an object. Servers should convert recognized - schemas to the latest internal value, and may reject unrecognized - values. - More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - properties: - description: - description: Description of the Environment - type: string - solutions: - description: Array of Solution versions deployed in this Environment - type: array - items: - type: object - properties: - name: - description: The Solution name - type: string - version: - description: The Solution version - type: string - required: - - name - - version - type: object - required: - - solutions - status: - type: object - version: v1alpha1 - versions: - - name: v1alpha1 - served: true - storage: true diff --git a/salt/metalk8s/orchestrate/solutions/available.sls b/salt/metalk8s/orchestrate/solutions/available.sls deleted file mode 100644 index a8c05df5b5..0000000000 --- a/salt/metalk8s/orchestrate/solutions/available.sls +++ /dev/null @@ -1,42 +0,0 @@ -{%- from "metalk8s/repo/macro.sls" import repo_prefix with context %} - -{%- set apiserver = 'https://' ~ pillar.metalk8s.api_server.host ~ ':6443' %} -{%- set version = pillar.metalk8s.nodes[pillar.bootstrap_id].version %} -{%- set kubeconfig = "/etc/kubernetes/admin.conf" %} -{%- set context = "kubernetes-admin@kubernetes" %} -{%- set custom_renderer = - "jinja | kubernetes kubeconfig=" ~ kubeconfig ~ "&context=" ~ context -%} - -# Init -Make sure "metalk8s-solutions" Namespace exists: - metalk8s_kubernetes.namespace_present: - - name: metalk8s-solutions - - kubeconfig: {{ kubeconfig }} - - context: {{ context }} - -Make sure "ui-branding" ConfigMap exists: - metalk8s_kubernetes.configmap_present: - - name: ui-branding - - namespace: metalk8s-solutions - - data: - config.json: | - { - "url": "{{ apiserver }}", - "registry_prefix": "{{ repo_prefix }}" - } - theme.json: | - { - "brand": {"primary": "#403e40", "secondary": "#e99121"} - } - - kubeconfig: {{ kubeconfig }} - - context: {{ context }} - - require: - - metalk8s_kubernetes: Make sure "metalk8s-solutions" Namespace exists - -Mount declared Solutions archives: - salt.state: - - tgt: {{ pillar.bootstrap_id }} - - sls: - - metalk8s.solutions.mounted - - saltenv: metalk8s-{{ version }} \ No newline at end of file diff --git a/salt/metalk8s/orchestrate/solutions/deploy-components.sls b/salt/metalk8s/orchestrate/solutions/deploy-components.sls new file mode 100644 index 0000000000..517fa63eac --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/deploy-components.sls @@ -0,0 +1,164 @@ +{%- from "metalk8s/map.jinja" import repo with context %} + +include: + - metalk8s.addons.solutions.deployed + +{%- set kubernetes_present_renderer = "jinja | metalk8s_kubernetes" %} +{%- set kubernetes_absent_renderer = + kubernetes_present_renderer ~ " absent=True" %} + +{%- set ui_relpath = "ui" %} +{%- set ui_files = ["deployment.yaml", "service.yaml"] %} +{%- set crds_relpath = "operator/deploy/crds" %} +{%- set crds_name_pattern = "*_crd.yaml" %} + +{# Operation macros #} +{%- macro manipulate_solution_components(solution, present=true) %} + {%- if present %} + {%- set action = "Apply" %} + {%- set renderer = kubernetes_present_renderer %} + {%- else %} + {%- set action = "Remove" %} + {%- set renderer = kubernetes_absent_renderer %} + {%- endif %} + + {# CRDs management #} + {# TODO: consider API version changes #} + {%- set crds_path = salt.file.join(solution.mountpoint, crds_relpath) %} + {%- set crd_files = salt.saltutil.cmd( + tgt=pillar.bootstrap_id, + fun='file.find', + kwarg={ + 'path': crds_path, + 'name': crds_name_pattern, + }, + )[pillar.bootstrap_id]['ret'] %} + + {%- for crd_file in crd_files %} + {%- set sls_content = salt.saltutil.cmd( + tgt=pillar.bootstrap_id, + fun='slsutil.renderer', + kwarg={ + 'path': crd_file, + 'default_renderer': renderer, + }, + )[pillar.bootstrap_id]['ret'] %} +{{ action }} CRD "{{ crd_file }}" for Solution {{ solution.name }}: + module.run: + - state.template_str: + - tem: "{{ sls_content | yaml }}" + + {%- endfor %} {# crd_file in crd_files #} + + {# TODO: StorageClasses, Grafana dashboards, ... #} +{%- endmacro %} + +{# TODO: this can be improved using the state module from #1713 #} +{%- macro deploy_solution_components(solution) %} + {{ manipulate_solution_components(solution, present=true) }} +{%- endmacro %} + +{# TODO: only retrieve object names and delete them without the renderer #} +{%- macro remove_solution_components(solution) %} + {{ manipulate_solution_components(solution, present=false) }} +{%- endmacro %} + +{# Helpers #} +{%- macro fail_missing_solution(name) %} +Cannot deploy components for Solution {{ name }}: + test.fail_without_changes: + - name: No version for Solution {{ name }} available + +{%- endmacro %} + +{%- macro fail_missing_version(name, version) %} +Cannot deploy desired version '{{ version }}' for Solution {{ name }}: + test.fail_without_changes: + - name: Solution {{ name }}-{{ version }} is not available + +{%- endmacro %} + +{%- macro get_latest(versions) %} + {# NOTE: would need Jinja 2.10 to have namespace objects #} + {%- set ns = {'candidate': none} %} + {%- for version in versions | map(attribute='version') %} + {%- if ns.candidate is none %} + {%- do ns.update({'candidate': version}) %} + {%- elif salt.pkg.version_cmp(version, ns.candidate) > 0 %} + {%- do ns.update({'candidate': version}) %} + {%- endif %} + {%- endfor -%} + {{ ns.candidate if ns.candidate is not none else '' }} +{%- endmacro %} + +{# Actual state formula #} +{%- if '_errors' in pillar.metalk8s.solutions %} +Cannot proceed with deployment of Solution cluster-wide components: + test.configurable_test_state: + - name: Cannot proceed due to pillar errors + - changes: False + - result: False + - comment: "Errors: {{ pillar.metalk8s.solutions._errors | join('; ') }}" + +{%- else %} + {# Dict of (name, version) pairs, where version can be set to 'latest' #} + {%- set desired = pillar.metalk8s.solutions.config.active %} + {%- set available = pillar.metalk8s.solutions.available %} + + {%- for name, versions in available.items() %} + {%- set desired_version = desired.get(name) %} + {%- if desired_version == 'latest' %} + {%- set desired_version = get_latest(versions) | trim %} + {%- endif %} + + {%- if not desired_version %} + {# Solution is not present in config.active #} + {%- set active_versions = versions + | selectattr('active', 'equalto', true) + | list %} + {%- if active_versions %} + {# There should only be one #} + {%- set solution = active_versions | first %} + {{- remove_solution_components(solution) }} + {%- endif %} + {%- else %} + {%- if desired_version not in versions | map(attribute='version') %} + {{- fail_missing_version(name, desired_version) }} + {%- else %} + {%- set solution = versions + | selectattr('version', 'equalto', desired_version) + | first %} + {{- deploy_solution_components(solution) }} + {%- endif %} + {%- endif %} + + {# Prepare list of available versions to set in the ConfigMap #} + {%- set updated_versions = [] %} + {%- for version in versions %} + {%- set updated_version = version %} + {# `active` will always be false if `desired_version` is None or doesn't + match any available version #} + {%- do updated_version.update({ + 'active': version.version == desired_version + }) %} + {%- do updated_versions.append(updated_version)%} + {%- endfor %} + +Update metalk8s-solutions ConfigMap for Solution {{ name }}: + metalk8s_kubernetes.object_updated: + - name: metalk8s-solutions + - namespace: metalk8s-solutions + - kind: ConfigMap + - apiVersion: v1 + - patch: + data: + {{ name }}: >- + {{ updated_versions | tojson }} + + {%- endfor %} + + {%- for name in desired | difference(available) %} + {{- fail_missing_solution(name) }} + {%- endfor %} + +{%- endif %} {# _errors in pillar #} diff --git a/salt/metalk8s/orchestrate/solutions/files/operator/configmap.yaml b/salt/metalk8s/orchestrate/solutions/files/operator/configmap.yaml new file mode 100644 index 0000000000..749cfad7e5 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/operator/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ solution }}-operator-config + namespace: {{ namespace }} +data: + operator.yaml: | + apiVersion: solutions.metalk8s.scality.com/v1alpha1 + kind: OperatorConfig + repositories: +{%- for version_info in pillar.metalk8s.solutions.available.get(solution, []) %} + - version: {{ version_info.version }} + repository: {{ registry }}/{{ version_info.id }} +{%- endfor %} diff --git a/salt/metalk8s/orchestrate/solutions/files/operator/deployment.yaml b/salt/metalk8s/orchestrate/solutions/files/operator/deployment.yaml new file mode 100644 index 0000000000..95a55bb691 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/operator/deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ solution }}-operator + namespace: {{ namespace }} +spec: + replicas: 1 + selector: + matchLabels: + name: {{ solution }}-operator + template: + metadata: + labels: + name: {{ solution }}-operator + spec: + serviceAccountName: {{ solution }}-operator + containers: + - name: {{ solution }}-operator + image: {{ repository }}/{{ image_name }}:{{ image_tag }} + command: + - {{ solution }}-operator + - --config=/etc/config/operator.yaml + imagePullPolicy: Always + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "{{ solution }}-operator" + volumeMounts: + - name: operator-config + mountPath: /etc/config + volumes: + - name: operator-config + configMap: + name: {{ solution }}-operator-config \ No newline at end of file diff --git a/salt/metalk8s/orchestrate/solutions/files/operator/role.yaml b/salt/metalk8s/orchestrate/solutions/files/operator/role.yaml new file mode 100644 index 0000000000..ca29a203fd --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/operator/role.yaml @@ -0,0 +1,50 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ solution }}-operator + namespace: {{ namespace }} +rules: +{# TODO: make those configurable #} +- apiGroups: + - "" + resources: + - pods + - services + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + verbs: + - '*' +- apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - '*' +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create +- apiGroups: + - apps + resourceNames: + - {{ solution }}-operator + resources: + - deployments/finalizers + verbs: + - update +{%- if custom_api_groups %} +- apiGroups: {{ custom_api_groups | tojson }} + resources: + - '*' + verbs: + - '*' +{%- endif %} diff --git a/salt/metalk8s/orchestrate/solutions/files/operator/role_binding.yaml b/salt/metalk8s/orchestrate/solutions/files/operator/role_binding.yaml new file mode 100644 index 0000000000..fbe46e645b --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/operator/role_binding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: {{ solution }}-operator + namespace: {{ namespace }} +subjects: +- kind: ServiceAccount + name: {{ solution }}-operator +roleRef: + kind: Role + name: {{ solution }}-operator + apiGroup: rbac.authorization.k8s.io diff --git a/salt/metalk8s/orchestrate/solutions/files/operator/service_account.yaml b/salt/metalk8s/orchestrate/solutions/files/operator/service_account.yaml new file mode 100644 index 0000000000..3e88d876af --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/operator/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ solution }}-operator + namespace: {{ namespace }} diff --git a/salt/metalk8s/orchestrate/solutions/files/ui/configmap.yaml b/salt/metalk8s/orchestrate/solutions/files/ui/configmap.yaml new file mode 100644 index 0000000000..d6573096c6 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/ui/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ solution }}-ui + namespace: {{ namespace }} +data: + config.json: | + {"url": "/api/kubernetes"} + theme.json: | + {"brand": {"primary": "#403e40", "secondary": "#e99121"}} diff --git a/salt/metalk8s/orchestrate/solutions/files/ui/deployment.yaml b/salt/metalk8s/orchestrate/solutions/files/ui/deployment.yaml new file mode 100644 index 0000000000..84c7e6beb2 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/ui/deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ solution }}-ui + namespace: {{ namespace }} + labels: + app: {{ solution }}-ui + heritage: {{ solution }} + app.kubernetes.io/name: {{ solution }}-ui + app.kubernetes.io/version: {{ version }} + app.kubernetes.io/component: ui + app.kubernetes.io/part-of: {{ solution }} + # UIs are deployed and managed by Salt, provided with MetalK8s + app.kubernetes.io/managed-by: salt +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ solution }}-ui + template: + metadata: + labels: + app: {{ solution }}-ui + heritage: {{ solution }} + app.kubernetes.io/name: {{ solution }}-ui + app.kubernetes.io/instance: {{ solution }}-ui + app.kubernetes.io/version: {{ version }} + app.kubernetes.io/component: ui + app.kubernetes.io/part-of: {{ solution }} + # UIs are deployed and managed by Salt, provided with MetalK8s + app.kubernetes.io/managed-by: salt + spec: + tolerations: + # UIs are deployed on "infra" Nodes, so we need these tolerations + - key: "node-role.kubernetes.io/bootstrap" + operator: "Exists" + effect: "NoSchedule" + - key: "node-role.kubernetes.io/infra" + operator: "Exists" + effect: "NoSchedule" + nodeSelector: + node-role.kubernetes.io/infra: '' + containers: + - name: {{ solution }}-ui + image: "{{ repository }}/{{ image_name }}:{{ image_tag }}" + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + name: http + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + scheme: HTTP + readinessProbe: + httpGet: + path: / + port: http + scheme: HTTP + volumeMounts: + - name: ui-config + mountPath: /etc/{{ solution }}/ui + readOnly: true + volumes: + - name: ui-config + configMap: + name: {{ solution }}-ui diff --git a/salt/metalk8s/orchestrate/solutions/files/ui/ingress.yaml b/salt/metalk8s/orchestrate/solutions/files/ui/ingress.yaml new file mode 100644 index 0000000000..05aa0b4f12 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/ui/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: {{ solution }}-ui + namespace: {{ namespace }} + labels: + app.kubernetes.io/managed-by: salt + app.kubernetes.io/name: {{ solution }}-ui + app.kubernetes.io/part-of: {{ solution }} + heritage: {{ solution }} + annotations: + nginx.ingress.kubernetes.io/rewrite-target: '/$2' + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + kubernetes.io/ingress.class: "nginx-control-plane" +spec: + rules: + - http: + paths: + - path: /env/{{ environment }}/{{ solution }}(/|$)(.*) + backend: + serviceName: {{ solution }}-ui + servicePort: 80 diff --git a/salt/metalk8s/orchestrate/solutions/files/ui/service.yaml b/salt/metalk8s/orchestrate/solutions/files/ui/service.yaml new file mode 100644 index 0000000000..7015289a6a --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/files/ui/service.yaml @@ -0,0 +1,21 @@ +kind: Service +apiVersion: v1 +metadata: + name: {{ solution }}-ui + namespace: {{ namespace }} + labels: + app: {{ solution }}-ui + heritage: {{ solution }} + app.kubernetes.io/name: {{ solution }}-ui + app.kubernetes.io/version: {{ version }} + app.kubernetes.io/component: ui + app.kubernetes.io/part-of: {{ solution }} + app.kubernetes.io/managed-by: salt +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 80 + selector: + app: {{ solution }}-ui + type: NodePort diff --git a/salt/metalk8s/orchestrate/solutions/init.sls b/salt/metalk8s/orchestrate/solutions/init.sls deleted file mode 100644 index ec07b88e9d..0000000000 --- a/salt/metalk8s/orchestrate/solutions/init.sls +++ /dev/null @@ -1,253 +0,0 @@ -{%- from "metalk8s/repo/macro.sls" import repo_prefix with context %} - -{%- set version = pillar.metalk8s.nodes[pillar.bootstrap_id].version %} -{%- set kubeconfig = "/etc/kubernetes/admin.conf" %} -{%- set context = "kubernetes-admin@kubernetes" %} -{%- set custom_renderer = - "jinja | kubernetes kubeconfig=" ~ kubeconfig ~ "&context=" ~ context -%} -{%- set configured = pillar.metalk8s.solutions.configured | default([]) %} -{%- set deployed = pillar.metalk8s.solutions.deployed | default({}) %} - - -# Configure -Prepare registry configuration for declared Solutions: - salt.state: - - tgt: {{ pillar.bootstrap_id }} - - sls: - - metalk8s.solutions.configured - - saltenv: metalk8s-{{ version }} - -# Compute information about Solutions from their ISO files -{%- set solutions_info = {} %} {# indexed by - #} -{%- set highest_versions = {} %} {# indexed by #} -{%- for solution_iso in configured %} - {%- set iso_info = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='metalk8s.archive_info_from_iso', - kwarg={ - 'path': solution_iso, - }, - )[pillar.bootstrap_id]['ret'] - %} - {%- set solution_name = iso_info.name | lower | replace(' ', '-') %} - {%- set fullname = solution_name ~ "-" ~ iso_info.version %} - {%- do solutions_info.update({ - fullname: { - 'name': solution_name, - 'version': iso_info.version, - 'iso': solution_iso, - } - }) %} - - {%- set highest_version = highest_versions.get(solution_name) %} - {%- if not highest_version %} - {%- do highest_versions.update({solution_name: iso_info.version}) %} - {%- elif salt.pkg.version_cmp(iso_info.version, highest_version) > 0 %} - {# TODO: study results with release tags included in the version #} - {%- do highest_versions.update({solution_name: iso_info.version}) %} - {%- endif %} -{%- endfor %} - -# Deploy components and update ConfigMap -{%- for fullname, solution_info in solutions_info.items() %} - {%- set was_deployed = false %} - {%- if solution_info.version == highest_versions[solution_info.name] %} - # Deploy the Admin UI - {%- set deploy_files_list = ["deployment.yaml", "service.yaml"] %} - {%- for deploy_file in deploy_files_list %} - {%- set filepath = "/srv/scality/" ~ fullname ~ "/ui/" ~ deploy_file %} - {%- set repository = repo_prefix ~ "/" ~ fullname %} - {%- set sls_content = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='slsutil.renderer', - kwarg={ - 'path': filepath, - 'default_renderer': custom_renderer, - 'repository': repository - }, - )[pillar.bootstrap_id]['ret'] - %} -Apply Admin UI "{{ deploy_file }}" for Solution {{ fullname }}: - module.run: - - state.template_str: - - tem: "{{ sls_content | yaml }}" - - require: - - salt: Prepare registry configuration for declared Solutions - - require_in: - - module: Register Solution {{ fullname }} in ConfigMap - - {%- endfor %} {# deploy_file in deploy_files_list #} - - # Deploy the CRDs - {%- set crds_path = "/srv/scality/" ~ fullname ~ "/operator/deploy/crds" %} - {%- set crd_files = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='file.find', - kwarg={ - 'path': crds_path, - 'name': '*_crd.yaml' - }, - )[pillar.bootstrap_id]['ret'] - %} - - {%- for crd_file in crd_files %} - {%- set sls_content = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='slsutil.renderer', - kwarg={ - 'path': crd_file, - 'default_renderer': custom_renderer - }, - )[pillar.bootstrap_id]['ret'] - %} -Apply CRD "{{ crd_file }}" for Solution {{ fullname }}: - module.run: - - state.template_str: - - tem: "{{ sls_content | yaml }}" - - require: - - salt: Prepare registry configuration for declared Solutions - - require_in: - - module: Register Solution {{ fullname }} in ConfigMap - - {%- endfor %} {# crd_file in crd_files #} - - {%- set was_deployed = true %} - {%- endif %} {# this version is the highest for this name #} - -Register Solution {{ fullname }} in ConfigMap: - module.run: - - metalk8s_solutions.register_solution_version: - - name: {{ solution_info.name }} - - version: {{ solution_info.version }} - - archive_path: {{ solution_info.iso }} - - deployed: {{ was_deployed }} - - kubeconfig: {{ kubeconfig }} - - context: {{ context }} - - require: - - salt: Prepare registry configuration for declared Solutions -{%- endfor %} {# fullname, solution_info in solutions_info.items() #} - - -# undeploy solutions -{%- set custom_absent_renderer = - "jinja | kubernetes kubeconfig=" ~ kubeconfig ~ "&context=" ~ context ~ "&absent=True" -%} - -# Undeoloy solution -# Only undeploy if this is the last deployed version -# Else the deployed version will already override the -# old k8s objects from older versions -{%- if deployed %} -{%- for solution_name, versions in deployed.items() %} - {%- if versions|length == 1 %} - {%- set version_info = versions[0] %} -# {%- for version_info in versions %} - {%- set lower_name = solution_name | lower | replace(' ', '-') %} - {%- set fullname = lower_name ~ '-' ~ version_info.version %} - {%- if version_info.iso not in configured %} - # Undeploy the Admin UI - {%- set deploy_files_list = ["deployment.yaml", "service.yaml"] %} - {%- for deploy_file in deploy_files_list %} - {%- set filepath = "/srv/scality/" ~ fullname ~ "/ui/" ~ deploy_file %} - {%- set repository = repo_prefix ~ "/" ~ fullname %} - {%- set sls_content = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='slsutil.renderer', - kwarg={ - 'path': filepath, - 'default_renderer': custom_absent_renderer, - 'repository': repository - }, - )[pillar.bootstrap_id]['ret'] - %} - -Delete Admin UI "{{ deploy_file }}" for Solution {{ fullname }}: - module.run: - - state.template_str: - - tem: "{{ sls_content | yaml }}" - - require: - - salt: Prepare registry configuration for declared Solutions - - {%- endfor %} {# deploy_file in deploy_files_list #} - - # Deploy the CRDs - {%- set crds_path = "/srv/scality/" ~ fullname ~ "/operator/deploy/crds" %} - {%- set crd_files = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='file.find', - kwarg={ - 'path': crds_path, - 'name': '*_crd.yaml' - }, - )[pillar.bootstrap_id]['ret'] - %} - - {%- for crd_file in crd_files %} - {%- set sls_content = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='slsutil.renderer', - kwarg={ - 'path': crd_file, - 'default_renderer': custom_absent_renderer - }, - )[pillar.bootstrap_id]['ret'] - %} -Delete CRD "{{ crd_file }}" for Solution {{ fullname }}: - module.run: - - state.template_str: - - tem: "{{ sls_content | yaml }}" - - {%- endfor %} {# crd_file in crd_files #} - {%- endif %} {# if version_info #} -# {%- endfor %} {# for version_info #} - {%- endif %} {# if len() #} - {%- endfor %} {# for solution_name #} - -{%- endif %} {# if deployed #} - - -# Unconfigure -Remove registry configurations for removed Solutions: - salt.state: - - tgt: {{ pillar.bootstrap_id }} - - sls: - - metalk8s.solutions.unconfigured - - saltenv: metalk8s-{{ version }} - -# Unmount -Unmount removed Solutions archives: - salt.state: - - tgt: {{ pillar.bootstrap_id }} - - sls: - - metalk8s.solutions.unmounted - - saltenv: metalk8s-{{ version }} - - require: - - salt: Remove registry configurations for removed Solutions - -# Unregister removed Solutions -{%- for solution_name, versions in deployed.items() %} - {%- for solution_info in versions %} - {%- if solution_info.iso not in configured %} -Unregister Solution {{ solution_name }}-{{ solution_info.version }}: - module.run: - - metalk8s_solutions.unregister_solution_version: - - name: {{ solution_name }} - - version: {{ solution_info.version }} - - kubeconfig: {{ kubeconfig }} - - context: {{ context }} - - require: - - salt: Unmount removed Solutions archives - {%- endif %} - {%- endfor %} {# solution_info in versions #} -{%- endfor %} {# solution_name, versions in deployed.items() #} - -Update registry with latest Solutions: - salt.state: - - tgt: {{ pillar.bootstrap_id }} - - sls: - - metalk8s.repo.installed - - saltenv: metalk8s-{{ version }} - - require: - - salt: Prepare registry configuration for declared Solutions - - salt: Remove registry configurations for removed Solutions diff --git a/salt/metalk8s/orchestrate/solutions/prepare-environment.sls b/salt/metalk8s/orchestrate/solutions/prepare-environment.sls new file mode 100644 index 0000000000..682baa2f20 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/prepare-environment.sls @@ -0,0 +1,178 @@ +{%- from "metalk8s/map.jinja" import repo with context %} + +{%- set env_name = pillar.orchestrate.env_name %} + +{%- macro deploy_operator(namespace, solution) %} + {%- set solution_id = solution.name | lower | replace(' ', '-') %} + +Apply ServiceAccount for Operator of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/operator/service_account.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + +Apply Role for Operator of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/operator/role.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + custom_api_groups: {{ solution.config.customApiGroups }} + +Apply RoleBinding for Operator of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/operator/role_binding.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + - require: + - metalk8s_kubernetes: Apply ServiceAccount for Operator of Solution {{ solution.name }} + - metalk8s_kubernetes: Apply Role for Operator of Solution {{ solution.name }} + +{# Store info for image repositories in some Operator ConfigMap + TODO: add documentation about this file #} +Apply Operator ConfigMap for Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/operator/configmap.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + registry: {{ repo.registry_endpoint }} + +Apply Operator Deployment for Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/operator/deployment.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + version: {{ solution.version }} + namespace: {{ namespace }} + image_name: {{ solution.config.operator.image.name }} + image_tag: {{ solution.config.operator.image.tag }} + repository: {{ repo.registry_endpoint ~ '/' ~ solution.id }} + - require: + - metalk8s_kubernetes: Apply RoleBinding for Operator of Solution {{ solution.name }} + - metalk8s_kubernetes: Apply Operator ConfigMap for Solution {{ solution.name }} + +{%- endmacro %} + +{%- macro deploy_admin_ui(namespace, solution) %} + {%- set solution_id = solution.name | lower | replace(' ', '-') %} + +Apply ConfigMap for UI of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/ui/configmap.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + version: {{ solution.version }} + namespace: {{ namespace }} + +Apply Deployment for UI of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/ui/deployment.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + version: {{ solution.version }} + namespace: {{ namespace }} + image_name: {{ solution.config.ui.image.name }} + image_tag: {{ solution.config.ui.image.tag }} + repository: {{ repo.registry_endpoint ~ "/" ~ solution.id }} + +Apply Service for UI of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/ui/service.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + version: {{ solution.version }} + +Apply Ingress for UI of Solution {{ solution.name }}: + metalk8s_kubernetes.object_present: + - name: salt://{{ slspath }}/files/ui/service.yaml + - template: jinja + - defaults: + solution: {{ solution_id }} + namespace: {{ namespace }} + version: {{ solution.version }} + +{%- endmacro %} + +{%- if '_errors' in pillar.metalk8s.solutions.environments %} + +Cannot proceed with preparation of environment {{ env_name }}: + test.configurable_test_state: + - name: Cannot proceed due to pillar errors + - changes: False + - result: False + - comment: "Errors: {{ pillar.metalk8s.solutions._errors | join('; ') }}" + +{%- else %} + {%- set environment = + pillar.metalk8s.solutions.environments.get(env_name) %} + {%- if environment is none %} + +Cannot prepare environment {{ env_name }}: + test.fail_without_changes: + - name: Environment {{ env_name }} does not exist + + {%- else %} + {%- set env_namespaces = environment.get('namespaces', {}) %} + {%- if env_namespaces %} + {%- for namespace, ns_conf in env_namespaces.items() %} + {%- set env_config = ns_conf.get('config', {}) %} + {%- if env_config %} + {%- for name, version in env_config.items() %} + {%- set available_versions = + pillar.metalk8s.solutions.available.get(name, []) %} + {%- if not available_versions %} + +Cannot deploy Solution {{ name }} for environment {{ env_name }}: + test.fail_without_changes: + - name: Solution {{ name }} is not available + + {%- elif version not in available_versions + | map(attribute='version') %} + +Cannot deploy Solution {{ name }}-{{ version }} for environment {{ env_name }}: + test.fail_without_changes: + - name: Version {{ version }} is not available for Solution {{ name }} + + {%- else %} + {%- set solution = available_versions + | selectattr('version', 'equalto', version) + | first %} + + {{- deploy_operator(namespace, solution) }} + {{- deploy_admin_ui(namespace, solution) }} + + {%- endif %} + {%- endfor %} {# name, version in env_config #} + {%- else %} + +No Solution configured in namespace {{ namespace }} for environment {{ env_name }}: + test.succeed_without_changes: + - name: >- + ConfigMap 'metalk8s-environment' for environment {{ env_name }} in + namespace {{ namespace }} is absent or empty + + {%- endif %} {# env_config is empty #} + {%- endfor %} {# namespace, ns_conf in env_namespaces #} + {%- else %} + +No Solution configured for environment {{ env_name }}: + test.succeed_without_changes: + - name: >- + ConfigMap 'metalk8s-environment' for environment {{ env_name }} + do not exists + + {%- endif %} {# env_namespaces is empty #} + {%- endif %} {# environment is none #} +{%- endif %} diff --git a/salt/metalk8s/repo/files/repositories-manifest.yaml.j2 b/salt/metalk8s/repo/files/repositories-manifest.yaml.j2 index 5eb8cf3f61..dd74e91114 100644 --- a/salt/metalk8s/repo/files/repositories-manifest.yaml.j2 +++ b/salt/metalk8s/repo/files/repositories-manifest.yaml.j2 @@ -51,9 +51,9 @@ spec: mountPath: /srv/scality/{{ env }}/images/ {%- endfor %} {%- for name, versions in solutions.items() | sort(attribute='0') %} - {%- for version_info in versions | sort(attribute='version') %} - - name: registry-{{ name }}-{{ version_info.version | replace('.', '-') }} - mountPath: /srv/scality/{{ name }}-{{ version_info.version }}/images/ + {%- for info in versions | sort(attribute='version') %} + - name: registry-{{ info.id | replace('.', '-') }} + mountPath: /srv/scality/{{ info.id }}/images/ {%- endfor %} {%- endfor %} volumes: @@ -72,10 +72,10 @@ spec: type: Directory {%- endfor %} {%- for name, versions in solutions.items() | sort(attribute='0') %} - {%- for version_info in versions | sort(attribute='version') %} - - name: registry-{{ name }}-{{ version_info.version | replace('.', '-') }} + {%- for info in versions | sort(attribute='version') %} + - name: registry-{{ info.id | replace('.', '-') }} hostPath: - path: /srv/scality/{{ name }}-{{ version_info.version }}/images/ + path: /srv/scality/{{ info.id }}/images/ type: Directory {%- endfor %} {%- endfor %} diff --git a/salt/metalk8s/repo/installed.sls b/salt/metalk8s/repo/installed.sls index 3b939b4dca..3a9957bb11 100644 --- a/salt/metalk8s/repo/installed.sls +++ b/salt/metalk8s/repo/installed.sls @@ -4,7 +4,7 @@ {%- set repositories_version = '1.0.0' %} {%- set archives = salt.metalk8s.get_archives() %} -{%- set solutions = pillar.metalk8s.get('solutions', {}).get('deployed', {}) %} +{%- set solutions = pillar.metalk8s.get('solutions', {}).get('available', {}) %} {%- set docker_repository = 'docker.io/library' %} {%- set image_name = 'nginx' %} @@ -34,9 +34,9 @@ Install repositories manifest: - {{ salt.file.join(repo.config.directory, repo.config.common_registry) }} - {{ salt.file.join(repo.config.directory, '99-' ~ saltenv ~ '-registry.inc') }} {%- for name, versions in solutions.items() | sort(attribute='0') %} - {%- for version_info in versions | sort(attribute='version') %} + {%- for info in versions | sort(attribute='version') %} - {{ salt.file.join(repo.config.directory, - name ~ '-' ~ version_info.version ~ '-registry-config.inc') }} + info.id ~ '-registry-config.inc') }} {%- endfor %} {%- endfor %} - config_files_opt: diff --git a/salt/metalk8s/solutions/available.sls b/salt/metalk8s/solutions/available.sls new file mode 100644 index 0000000000..bd02c7c4f9 --- /dev/null +++ b/salt/metalk8s/solutions/available.sls @@ -0,0 +1,116 @@ +# +# Management of Solution archives, mount/unmount and expose container images +# +# Relies on the `archives` key from /etc/metalk8s/solutions.yaml +# + +{%- from "metalk8s/map.jinja" import repo with context %} + +{%- macro extract_info(archive_path) %} + {{ machine_name }},{{ display_name }},{{ mount_path }} +{%- endmacro %} + +{%- set available = pillar.metalk8s.solutions.available | d({}) %} +{%- set configured = pillar.metalk8s.solutions.config.archives | d([]) %} + +{%- if '_errors' in pillar.metalk8s.solutions.config %} +Cannot proceed with mounting of Solution archives: + test.fail_without_changes: + - comment: "Errors: {{ pillar.metalk8s.solutions.config._errors | join('; ') }}" + +{%- elif not available and not configured %} +No Solution found in configuration: + test.succeed_without_changes + +{%- else %} + {#- Mount configured #} + {%- for archive_path in configured %} + {%- set solution = salt['metalk8s.archive_info_from_iso'](archive_path) %} + {%- set machine_name = solution.name | replace(' ', '-') | lower %} + {%- set id = machine_name ~ '-' ~ solution.version %} + {%- set display_name = solution.name ~ ' ' ~ solution.version %} + {%- set mount_path = "/srv/scality/" ~ id -%} + +{# Mount the archive #} +Mountpoint for Solution {{ display_name }} exists: + file.directory: + - name: {{ mount_path }} + - makedirs: true + +Archive of Solution {{ display_name }} is mounted at {{ mount_path }}: + mount.mounted: + - name: {{ mount_path }} + - device: {{ archive_path }} + - fstype: iso9660 + - mkmnt: false + - opts: + - ro + - nofail + - persist: true + - match_on: + - name + - require: + - file: Mountpoint for Solution {{ display_name }} exists + +{# Validate the archive contents + TODO: This should be moved before mounting the solution's ISO, using some + custom module #} +Product information for Solution {{ display_name }} exists: + file.exists: + - name: {{ mount_path }}/product.txt + - require: + - mount: Archive of Solution {{ display_name }} is mounted at {{ mount_path }} + +Container images for Solution {{ display_name }} exist: + file.exists: + - name: {{ mount_path }}/images + - require: + - mount: Archive of Solution {{ display_name }} is mounted at {{ mount_path }} + +Expose container images for Solution {{ display_name }}: + file.managed: + - source: {{ mount_path }}/registry-config.inc.j2 + - name: {{ repo.config.directory }}/{{ id }}-registry-config.inc + - template: jinja + - defaults: + repository: {{ id }} + registry_root: {{ mount_path }}/images + - require: + - file: Container images for Solution {{ display_name }} exist + + {%- endfor %} {# Configured solutions are all mounted and images exposed #} + + {#- Unmount all Solution ISOs mounted in /srv/scality not referenced in + the configuration file #} + {%- for machine_name, versions in available.items() %} + {%- for info in versions %} + {%- if info.archive not in configured %} + {%- set display_name = info.name ~ ' ' ~ info.version %} + {%- if info.active %} +Cannot remove archive for active Solution {{ display_name }}: + test.fail_without_changes: + - name: Solution {{ display_name }} is still active, cannot remove it + + {%- else %} +Remove container images for Solution {{ display_name }}: + file.absent: + - name: {{ repo.config.directory }}/{{ info.id }}-registry-config.inc + +Unmount Solution {{ display_name }}: + mount.unmounted: + - name: {{ info.mountpoint }} + - persist: True + - require: + - file: Remove container images for Solution {{ display_name }} + +Clean mountpoint for Solution {{ display_name }}: + file.absent: + - name: {{ info.mountpoint }} + - require: + - mount: Unmount Solution {{ display_name }} + + {%- endif %} + {%- endif %} + {%- endfor %} + {%- endfor %} +{%- endif %} diff --git a/salt/metalk8s/solutions/configured.sls b/salt/metalk8s/solutions/configured.sls deleted file mode 100644 index feb8c0c392..0000000000 --- a/salt/metalk8s/solutions/configured.sls +++ /dev/null @@ -1,24 +0,0 @@ -{%- from "metalk8s/map.jinja" import repo with context %} - -{%- set solutions_list = pillar.metalk8s.solutions.configured %} -{%- if solutions_list %} -{%- for solution_iso in solutions_list %} - {%- set solution = salt['metalk8s.archive_info_from_iso'](solution_iso) %} - {%- set lower_name = solution.name | lower | replace(' ', '-') %} - {%- set full_name = lower_name ~ '-' ~ solution.version %} - {%- set path = "/srv/scality/" ~ full_name %} -Configure nginx for Solution {{ full_name }}: - file.managed: - - source: {{ path }}/registry-config.inc.j2 - - name: {{ repo.config.directory }}/{{ full_name }}-registry-config.inc - - template: jinja - - defaults: - repository: {{ full_name }} - registry_root: {{ path }}/images - -{%- endfor %} -{%- else %} -No configured Solution: - test.succeed_without_changes: - - name: Nothing to do -{% endif %} diff --git a/salt/metalk8s/solutions/init.sls b/salt/metalk8s/solutions/init.sls index e69de29bb2..40477a7993 100644 --- a/salt/metalk8s/solutions/init.sls +++ b/salt/metalk8s/solutions/init.sls @@ -0,0 +1,31 @@ +# +# Management of Solutions on MetalK8s +# =================================== +# +# Available states +# ---------------- +# +# * available -> mount and expose images for configured Solution archives +# +# Configuration +# ------------- +# +# These states depend on the `SolutionsConfiguration` file, located at +# /etc/metalk8s/solutions.yaml, to know which Solution archive to +# add/remove. It has the following format: +# +# ..code-block:: yaml +# +# apiVersion: metalk8s.scality.com/v1alpha1 +# kind: SolutionsConfiguration +# archives: +# - /path/to/solution/archive.iso +# active: +# solution-name: X.Y.Z-suffix (or 'latest') +# +# Refer to each state documentation for more info on how actions are computed +# based on this file's contents and the system's state. +# + +include: + - .available diff --git a/salt/metalk8s/solutions/mounted.sls b/salt/metalk8s/solutions/mounted.sls deleted file mode 100644 index 554d70eb6c..0000000000 --- a/salt/metalk8s/solutions/mounted.sls +++ /dev/null @@ -1,54 +0,0 @@ -{%- set solutions_list = pillar.metalk8s.solutions.configured %} -{%- if solutions_list %} -{%- for solution_iso in solutions_list %} - {%- set solution = salt['metalk8s.archive_info_from_iso'](solution_iso) %} - {%- set lower_name = solution.name | lower | replace(' ', '-') %} - {%- set full_name = lower_name ~ '-' ~ solution.version %} - {%- set path = "/srv/scality/" ~ full_name %} -Solution mountpoint {{ path }} exists: - file.directory: - - name: {{ path }} - - makedirs: true - -Solution {{ solution_iso }} is available at {{ path }}: - mount.mounted: - - name: {{ path }} - - device: {{ solution_iso }} - - fstype: iso9660 - - mkmnt: false - - opts: - - ro - - nofail - - persist: true - - match_on: - - name - - require: - - file: Solution mountpoint {{ path }} exists - -# Validate solution structure -# TODO: This should be moved before mounting the solution's ISO -Assert '{{ path }}/product.txt' exists: - file.exists: - - name: {{ path }}/product.txt - - require: - - mount: Solution {{ solution_iso }} is available at {{ path }} - - -Assert '{{ path }}/images' exists: - file.exists: - - name: {{ path }}/images - - require: - - mount: Solution {{ solution_iso }} is available at {{ path }} - -Assert '{{ path }}/operator/deploy' exists: - file.exists: - - name: {{ path }}/operator/deploy - - require: - - mount: Solution {{ solution_iso }} is available at {{ path }} - -{%- endfor %} -{% else %} -No configured Solution: - test.succeed_without_changes: - - name: Nothing to do -{% endif %} \ No newline at end of file diff --git a/salt/metalk8s/solutions/unconfigured.sls b/salt/metalk8s/solutions/unconfigured.sls deleted file mode 100644 index 3830ece2e5..0000000000 --- a/salt/metalk8s/solutions/unconfigured.sls +++ /dev/null @@ -1,26 +0,0 @@ -{%- from "metalk8s/map.jinja" import repo with context %} - -{%- set configured = pillar.metalk8s.solutions.configured or [] %} -{%- set deployed = pillar.metalk8s.solutions.deployed or {} %} -{%- if deployed %} -{%- for solution_name, versions in deployed.items() %} - {%- for version_info in versions %} - {%- set lower_name = solution_name | lower | replace(' ', '-') %} - {%- set full_name = lower_name ~ '-' ~ version_info.version %} - {%- if version_info.iso not in configured %} -Unconfigure nginx for solution {{ full_name }}: - file.absent: - - name: {{ repo.config.directory }}/{{ full_name }}-registry-config.inc - - {%- else %} -Keeping configuration for solution {{ full_name }}: - test.succeed_without_changes: - - name: Nginx for solution {{ solution_name }} remains - - {%- endif %} - {%- endfor %} -{%- endfor %} -{%- else %} -No solution to unconfigure: - test.succeed_without_changes -{%- endif %} \ No newline at end of file diff --git a/salt/metalk8s/solutions/unmounted.sls b/salt/metalk8s/solutions/unmounted.sls deleted file mode 100644 index 6dc258346f..0000000000 --- a/salt/metalk8s/solutions/unmounted.sls +++ /dev/null @@ -1,34 +0,0 @@ -{%- set configured = pillar.metalk8s.solutions.configured or [] %} -{%- set deployed = pillar.metalk8s.solutions.deployed or {} %} -{%- if deployed %} -{%- for solution_name, versions in deployed.items() %} - {%- for version_info in versions %} - {%- set lower_name = solution_name | lower | replace(' ', '-') %} - {%- set full_name = lower_name ~ '-' ~ version_info.version %} - {%- if version_info.iso not in configured %} -# Solution already unconfigured, unmount it now -Umount solution {{ full_name }}: - mount.unmounted: - - name: /srv/scality/{{ full_name }} - - device: {{ version_info.iso }} - - persist: True - -Clean mount point for solution {{ full_name }}: - file.absent: - - name: /srv/scality/{{ full_name }} - - require: - - mount: Umount solution {{ full_name }} - - {%- else %} -Solution {{ full_name }} remains: - test.succeed_without_changes: - - name: {{ solution_name }} remains - - {%- endif %} - {%- endfor %} -{%- endfor %} -{%- else %} -No solution to unmount: - test.succeed_without_changes - -{%- endif %} \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh index d83057fbd5..666f70d563 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -339,3 +339,13 @@ configure_salt_minion_local_mode() { --local --retcode-passthrough state.sls metalk8s.salt.minion.local \ pillar="{'metalk8s': {'archives': '$BASE_DIR'}}" saltenv=base } + +get_salt_env() { + "$SALT_CALL" --out txt slsutil.renderer \ + string="metalk8s-{{ pillar.metalk8s.nodes[grains.id].version }}" \ + | cut -c 8- +} + +get_salt_minion_id() { + "$SALT_CALL" --out txt grains.get id | cut -c 8- +} diff --git a/scripts/solution-manager.sh b/scripts/solution-manager.sh deleted file mode 100755 index f80767e537..0000000000 --- a/scripts/solution-manager.sh +++ /dev/null @@ -1,270 +0,0 @@ -#!/bin/bash -set -e -set -u -set -o pipefail - -SOLUTION_CONFIG="/etc/metalk8s/solutions.yaml" -VERBOSE=${VERBOSE:-0} -LOGFILE="/var/log/metalk8s/solution-manager.log" -SALT_CALL=${SALT_CALL:-salt-call} -CRICTL=${CRICTL:-crictl} -SALT="" -SALTENV="" -PILLAR="" - - -_usage() { - echo "solution-manager.sh [options]" - echo "Options:" - echo "-a/--add : Add solution from ISO path" - echo "-d/--del : Uninstall solution from ISO path" - echo "-l/--log-file : Path to log file" - echo "-v/--verbose: Run in verbose mode" -} - -SOLUTIONS_ADD=() -SOLUTIONS_REMOVE=() -EXISTENT_SOLUTIONS=() -while (( "$#" )); do - case "$1" in - -a|--add) - SOLUTIONS_ADD+=("$2") - shift 2 - ;; - -d|--del) - SOLUTIONS_REMOVE+=("$2") - shift 2 - ;; - -v|--verbose) - VERBOSE=1 - shift - ;; - -l|--log-file) - LOGFILE="$2" - [ -z "$DEBUG" ] && DEBUG="on" - shift 2 - ;; - *) # unsupported flags - echo "Error: Unsupported flag $1" >&2 - _usage - exit 1 - ;; - esac -done - -TMPFILES=$(mktemp -d) - -mkdir -p "$(dirname "${LOGFILE}")" - -cat << EOF >> "${LOGFILE}" ---- MetalK8s solution manager started on $(date -u -R) --- -EOF - -exec > >(tee -ia "${LOGFILE}") 2>&1 - -cleanup() { - rm -rf "${TMPFILES}" || true -} - -trap cleanup EXIT - - -run_quiet() { - local name=$1 - shift 1 - - echo -n "> ${name}..." - local start - start=$(date +%s) - set +e - "$@" 2>&1 | tee -ia "${LOGFILE}" > "${TMPFILES}/out" - local RC=$? - set -e - local end - end=$(date +%s) - - local duration=$(( end - start )) - - if [ $RC -eq 0 ]; then - echo " done [${duration}s]" - else - echo " fail [${duration}s]" - cat >/dev/stderr << EOM - -Failure while running step '${name}' - -Command: $@ - -Output: - -<< BEGIN >> -EOM - cat "${TMPFILES}/out" > /dev/stderr - - cat >/dev/stderr << EOM -<< END >> - -This script will now exit - -EOM - - exit 1 - fi -} - -run_verbose() { - local name=$1 - shift 1 - - echo "> ${name}..." - "$@" -} - -run() { - if [ "$VERBOSE" -eq 1 ]; then - run_verbose "${@}" - else - run_quiet "${@}" - fi -} - -die() { - echo 1>&2 "$@" - return 1 -} - -# helper function to set the current saltenv -_set_env() { - if [ -z "$SALTENV" ]; then - SALTENV="metalk8s-$($SALT_CALL --out txt slsutil.renderer \ - string="{{ pillar.metalk8s.nodes[grains.id].version }}" \ - | cut -c 8-)" - fi -} - -_check_salt_master() { - [ -z "$SALTENV" ] && die "Cannot detect current salt env" - # check if salt master is up - master_ps=$($CRICTL ps -q --label io.kubernetes.container.name=salt-master) - [ -z "$master_ps" ] && die "Cannot find salt master container" - SALT="$CRICTL exec $master_ps " - return 0 -} - -_set_bootstrap_id() { - local -r bootstrap_id=$( - ${SALT_CALL} --local --out txt grains.get id \ - | awk '/^local\: /{ print $2 }' - ) - - PILLAR=( - "{" - " 'bootstrap_id': '$bootstrap_id'" - "}" - ) - -} -_init () { - _set_env - _check_salt_master - _set_bootstrap_id - if [ ! -f "$SOLUTION_CONFIG" ]; then - echo "archives: []" >"$SOLUTION_CONFIG" - fi -} - - -# helper function to check for element in array -containsElement () { - local element match="$1" - shift - for element in "$@"; do - [[ "$element" == "$match" ]] && return 0; - done - return 1 -} - -add_solutions() { - add=("$@") - for solution in "${add[@]}"; do - if ! containsElement "'$solution'" \ - "${EXISTENT_SOLUTIONS[@]+"${EXISTENT_SOLUTIONS[@]}"}"; then - EXISTENT_SOLUTIONS+=("'$solution'") - fi - done -} - -remove_solutions() { - delete=("$@") - for target in "${delete[@]}"; do - for i in "${!EXISTENT_SOLUTIONS[@]}"; do - if [[ "${EXISTENT_SOLUTIONS[i]}" = "$target" ]]; then - unset 'EXISTENT_SOLUTIONS[i]' - fi - done - done - # Rebuild the gaps in the array - for i in "${!EXISTENT_SOLUTIONS[@]}"; do - new_array+=( "${EXISTENT_SOLUTIONS[i]}" ) - done - EXISTENT_SOLUTIONS=("${new_array[@]+"${new_array[@]}"}") - unset new_array -} - -_add_del_solution() { - # Skip adding/deleting solutions if none is passed - [ ${#SOLUTIONS_REMOVE[@]} -lt 1 ] && [ ${#SOLUTIONS_ADD[@]} -lt 1 ] && return 0 - # The use salt file.serialize merge require having full list - # salt-call output example: - # local: ["/tmp/solution1.iso", "/tmp/solution2.iso"] - # parsed products: - # ("/tmp/solution1.iso" "/tmp/solution2.iso") - IFS=" " read -r -a \ - EXISTENT_SOLUTIONS <<< "$(salt-call --out txt slsutil.renderer \ - string="{{ pillar.metalk8s.solutions.configured | join(' ') }}" | cut -d' ' -f2- | tr -d '{}' )" - # Add new solutions - if [ ${#SOLUTIONS_ADD[@]} -ge 1 ]; then - add_solutions "${SOLUTIONS_ADD[@]}" - fi - # Remove unwanted solutions - if [ ${#SOLUTIONS_REMOVE[@]} -ge 1 ]; then - remove_solutions "${SOLUTIONS_REMOVE[@]}" - fi - echo "Collecting solutions..." - # build product list - if [ ${#EXISTENT_SOLUTIONS[@]} -eq 0 ]; then - solutions_list="" - else - solutions_list=${EXISTENT_SOLUTIONS[0]} - for i in "${EXISTENT_SOLUTIONS[@]:1}"; do - solutions_list+=,$i - done - fi - - echo "Updating $SOLUTION_CONFIG" - $SALT_CALL state.single file.serialize "$SOLUTION_CONFIG" \ - dataset="{'archives': [$solutions_list]}" \ - merge_if_exists=True \ - formatter=yaml \ - show_changes=True \ - --retcode-passthrough - return $? -} - -_configure_solutions() { - echo "Mount solutions..." - $SALT salt-run state.orchestrate \ - metalk8s.orchestrate.solutions.available \ - saltenv="$SALTENV" \ - pillar="${PILLAR[*]}" - echo "Configure and deploy solutions..." - $SALT salt-run state.orchestrate \ - metalk8s.orchestrate.solutions \ - saltenv="$SALTENV" \ - pillar="${PILLAR[*]}" -} - -# Main -_init -run "Add/Delete solution" _add_del_solution -run "Configure solutions" _configure_solutions diff --git a/scripts/solutions.sh b/scripts/solutions.sh new file mode 100755 index 0000000000..677f27f6ed --- /dev/null +++ b/scripts/solutions.sh @@ -0,0 +1,500 @@ +#!/bin/bash + +set -euo pipefail + +KUBECONFIG=${KUBECONFIG:-/etc/kubernetes/admin.conf} +SALT_CALL=${SALT_CALL:-salt-call} +NAMESPACE_REGEX='^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' +ENV_ANNOTATION=solutions.metalk8s.scality.com/environment-description +ENV_LABEL=solutions.metalk8s.scality.com/environment +ENV_CONFIGMAP=metalk8s-environment +SOLUTIONS_NAMESPACE=metalk8s-solutions +SOLUTIONS_CONFIGMAP=metalk8s-solutions + +ARCHIVES=() +DESCRIPTION='' +LOGFILE=/var/log/metalk8s/solutions.log +NAME='' +NAMESPACE='' +SOLUTION='' +VERBOSE=${VERBOSE:-0} +VERSION='' + +declare -A COMMANDS=( + [import]=import_solution + [unimport]=unimport_solution + [activate]=activate_solution + [deactivate]=deactivate_solution + [create-env]=create_environment + [delete-env]=delete_environment + [add-solution]=add_solution + [delete-solution]=delete_solution +) +declare -A COMMAND_MANDATORY_OPTIONS=( + [import]='--archive' + [unimport]='--archive' + [activate]='--name --version' + [deactivate]='--name' + [create-env]='--name' + [delete-env]='--name' + [add-solution]='--name --solution --version' + [delete-solution]='--name --solution' +) +declare -A OPTIONS_MAPPING=( + [--archive]=ARCHIVES + [--description]=DESCRIPTION + [--name]=NAME + [--namespace]=NAMESPACE + [--solution]=SOLUTION + [--version]=VERSION +) + +usage() { + SCRIPT_NAME=${BASH_SOURCE[0]##*/} + + echo "Usage: $SCRIPT_NAME COMMAND [OPTIONS]" + echo + echo "Commands:" + echo " activate Deploy a Solution components (UI, Operator," + echo " CRDs, ...)" + echo " -n, --name Name of the Solution to deploy" + echo " -V, --version Version of the Solution to deploy" + echo + echo " deactivate Remove a Solution components (UI, Operator," + echo " CRDs, ...)" + echo " -n, --name Name of the Solution to delete" + echo " -V, --version Version of the Solution to delete" + echo + echo " import Import a Solution archive" + echo " -a, --archive Path to the Solution archive to import," + echo " option can be provided multiple times" + echo + echo " unimport Unimport a Solution archive" + echo " -a, --archive Path to the Solution archive to unimport," + echo " option can be provided multiple times" + echo + echo " create-env Create a new Environment" + echo " -d, --description Description of the Environment (optional)" + echo " -n, --name Name of the Environment to create" + echo " -N, --namespace Name of the Namespace to create inside" + echo " the Environment (optional, default to the" + echo " Environment name)" + echo + echo " delete-env Delete an Environment" + echo " -n, --name Name of the Environment to delete" + echo " -N, --namespace Name of the Namespace to delete from" + echo " the Environment (optional, if not provided," + echo " removes all Namespaces in the Environment)" + echo + echo " add-solution Add a Solution in an Environment" + echo " -n, --name Name of the Environment to add the Solution" + echo " in" + echo " -N, --namespace Namespace to add the Solution in (optional," + echo " if not provided default to the Environment" + echo " name)" + echo " -s, --solution Name of the Solution to add" + echo " -V, --version Version of the Solution to add" + echo + echo " delete-solution Delete a Solution from an Environment" + echo " -n, --name Name of the Environment to delete the" + echo " Solution from" + echo " -N, --namespace Namespace to delete the Solution from" + echo " (optional, if not provided default to the" + echo " Environment name)" + echo " -s, --solution Name of the Solution to delete" + echo + echo "General options:" + echo " -h, --help Display this message and exit" + echo " -l, --log-file File to write logs (default: $LOGFILE)" + echo " -v, --verbose Show commands launched, for debugging purposes" +} + +check_namespace_validity() { + [[ ${1:-} =~ $NAMESPACE_REGEX ]] +} + +LONG_OPTS=' + archive:, + description:, + help, + log-file:, + name:, + namespace:, + solution:, + verbose, + version: +' +SHORT_OPTS='a:d:hl:n:N:s:vV:' + +if ! options=$(getopt --options "$SHORT_OPTS" --long "$LONG_OPTS" -- "$@"); then + echo 1>&2 "Incorrect arguments provided" + usage + exit 1 +fi + +eval set -- "$options" + +while :; do + case $1 in + -a|--archive) + shift + ARCHIVES+=("$1") + ;; + -d|--description) + shift + DESCRIPTION=$1 + ;; + -h|--help) + usage + exit + ;; + -l|--log-file) + shift + LOGFILE=$1 + ;; + -n|--name) + shift + NAME=$1 + ;; + -N|--namespace) + shift + NAMESPACE=$1 + if ! check_namespace_validity "$NAMESPACE"; then + echo 1>&2 "Namespace name '$NAMESPACE' is invalid: it must" \ + "consist of lower case alphanumeric characters or '-'," \ + "and must start and end with an alphanumeric character." + exit 1 + fi + ;; + -s|--solution) + shift + SOLUTION=$1 + ;; + -v|--verbose) + VERBOSE=1 + ;; + -V|--version) + shift + VERSION=$1 + ;; + --) + shift + break + ;; + *) + echo 1>&2 "Option parsing failure" + exit 1 + ;; + esac + shift +done + +TMPFILES=$(mktemp -d) + +mkdir -p "$(dirname "$LOGFILE")" + +exec > >(tee -ia "$LOGFILE") 2>&1 + +cleanup() { + rm -rf "$TMPFILES" || true +} + +trap cleanup EXIT + +BASE_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +# shellcheck disable=SC1090 +. "$BASE_DIR"/common.sh + +check_command_mandatory_options() { + local -a missing_options=() + local -r command=$1 + + for option in ${COMMAND_MANDATORY_OPTIONS[$command]:-}; do + [[ ${!OPTIONS_MAPPING[$option]:-} ]] || missing_options+=("$option") + done + + if (( ${#missing_options[@]} )); then + echo 1>&2 "Missing options for command '$command':" \ + "${missing_options[@]}" + return 1 + fi + + return 0 +} + +salt_minion_exec() { + salt-call "$@" --retcode-passthrough +} + +salt_master_exec() { + SALTENV=${SALTENV:-$(get_salt_env)} + SALT_MASTER_CONTAINER_ID=${SALT_MASTER_CONTAINER_ID:-$(get_salt_container)} + crictl exec -i "$SALT_MASTER_CONTAINER_ID" "$@" saltenv="$SALTENV" +} + +activate_solution() { + run "Updating Solutions configuration file" \ + salt_minion_exec metalk8s_solutions.activate_solution \ + solution="$NAME" \ + version="$VERSION" \ + --local + + run "Deploying Solution components" \ + salt_master_exec salt-run state.orchestrate \ + metalk8s.orchestrate.solutions.deploy-components \ + pillar="{'bootstrap_id': '$(get_salt_minion_id)'}" +} + +deactivate_solution() { + run "Updating Solutions configuration file" \ + salt_minion_exec metalk8s_solutions.deactivate_solution \ + solution="$NAME" \ + --local + + run "Removing Solution components" \ + salt_master_exec salt-run state.orchestrate \ + metalk8s.orchestrate.solutions.deploy-components \ + pillar="{'bootstrap_id': '$(get_salt_minion_id)'}" +} + +configure_archives() { + local removed=${1:-False} + + for archive in "${ARCHIVES[@]}"; do + salt_minion_exec metalk8s_solutions.configure_archive \ + archive="$archive" \ + removed="$removed" \ + create_config=True \ + --local || return + done + + salt_minion_exec saltutil.refresh_pillar +} + +import_solution() { + SALTENV=${SALTENV:-$(get_salt_env)} + + run "Updating Solutions configuration file" configure_archives + run "Importing Solutions" \ + salt_minion_exec state.sls metalk8s.solutions.available \ + saltenv="$SALTENV" + run "Configuring Metalk8s registry" \ + salt_minion_exec state.sls metalk8s.repo.installed \ + saltenv="$SALTENV" +} + +unimport_solution() { + SALTENV=${SALTENV:-$(get_salt_env)} + + run "Updating Solutions configuration file" configure_archives True + run "Unimporting Solutions" \ + salt_minion_exec state.sls metalk8s.solutions.available \ + saltenv="$SALTENV" + run "Configuring Metalk8s registry" \ + salt_minion_exec state.sls metalk8s.repo.installed \ + saltenv="$SALTENV" +} + +namespace_is_in_environment() { + local -r namespace=$1 environment=$2 + + kubectl get namespace --selector="$ENV_LABEL=$environment" \ + --output=jsonpath='{ .items[*].metadata.name }' \ + | grep --quiet --word-regexp "$namespace" +} + +check_namespace() { + if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then + echo 1>&2 "Namespace '$NAMESPACE' does not exist" + return 1 + fi + + if ! namespace_is_in_environment "$NAMESPACE" "$NAME" &> /dev/null; then + echo 1>&2 "Namespace '$NAMESPACE' is not linked to the" + "Environment '$NAME'" + return 1 + fi +} + +create_environment() { + if ! [[ ${NAMESPACE:-} ]]; then + NAMESPACE=$NAME + if ! check_namespace_validity "$NAMESPACE"; then + echo 1>&2 "Environment name '$NAME' can't be used as a Namespace" \ + "name. Please provide a Namespace name using the --namespace" \ + "option, which only contains lower case alphanumeric" \ + "characters or '-', and starts and ends with an alphanumeric" \ + "character." + return 1 + fi + fi + + if kubectl get namespace "$NAMESPACE" &> /dev/null; then + echo 1>&2 "Namespace '$NAMESPACE' already exists" + return 1 + fi + + run "Creating Namespace '$NAMESPACE'" \ + kubectl create namespace "$NAMESPACE" + run "Adding Namespace '$NAMESPACE' to Environment '$NAME'" \ + kubectl label namespace "$NAMESPACE" "$ENV_LABEL=$NAME" + run "Setting Namespace '$NAMESPACE' description" \ + kubectl annotate namespace "$NAMESPACE" \ + "$ENV_ANNOTATION=${DESCRIPTION:-}" + run "Creating Namespace '$NAMESPACE' configuration" \ + kubectl create configmap "$ENV_CONFIGMAP" \ + --namespace "$NAMESPACE" +} + +delete_namespaces() { + for namespace; do + run "Deleting Namespace '$namespace'" \ + kubectl delete namespace "$namespace" + done +} + +delete_environment() { + local -a namespaces=() + + if ! [[ ${NAMESPACE:-} ]]; then + read -ra namespaces <<< "$( + kubectl get namespace \ + --selector="$ENV_LABEL=$NAME" \ + --output=jsonpath='{ .items[*].metadata.name }' + )" + if ! (( ${#namespaces[@]} )); then + echo 1>&2 "No such Environment '$NAME'" + return 1 + fi + else + check_namespace || return + namespaces=("$NAMESPACE") + fi + + delete_namespaces "${namespaces[@]}" +} + +check_solution_version_exists() { + local -r version=$1 solution_data=$2 + local -ra python_script=( + "import json, sys;" + "available = any(v.get('version') == sys.argv[1] for v in" + "json.loads(sys.argv[2]));" + "sys.exit(0 if available else 1);" + ) + + python -c "${python_script[*]}" "$version" "$solution_data" +} + +check_solution_exists() { + local solution_data + + solution_data=$( + kubectl get configmap "$SOLUTIONS_CONFIGMAP" \ + --namespace "$SOLUTIONS_NAMESPACE" \ + --output=jsonpath="{ .data['$SOLUTION'] }" + ) + + if ! [[ "$solution_data" ]]; then + echo 1>&2 "Solution '$SOLUTION' is not deployed" + return 1 + fi + + if ! check_solution_version_exists "$VERSION" "$solution_data"; then + echo 1>&2 "Version '$VERSION' of Solution '$SOLUTION' is not deployed" + return 1 + fi +} + +add_solution() { + [[ ${NAMESPACE:-} ]] || NAMESPACE=$NAME + + check_namespace && check_solution_exists || return + + local -ra patch=( + '{' + ' "data": {' + ' "'"$SOLUTION"'": "'"$VERSION"'"' + ' }' + '}' + ) + + run "Adding Solution '$SOLUTION:$VERSION'" \ + kubectl patch configmap "$ENV_CONFIGMAP" \ + --namespace "$NAMESPACE" \ + --patch "${patch[*]}" + + local -ra pillar=( + "{" + " 'orchestrate': {" + " 'env_name': '$NAME'" + " }" + "}" + ) + + run "Preparing Environment '$NAME'" \ + salt_master_exec salt-run state.orchestrate \ + metalk8s.orchestrate.solutions.prepare-environment \ + pillar="${pillar[*]}" +} + +delete_solution() { + [[ ${NAMESPACE:-} ]] || NAMESPACE=$NAME + + check_namespace || return + + if ! kubectl get configmap "$ENV_CONFIGMAP" \ + --namespace "$NAMESPACE" \ + --output=jsonpath="{.data.$SOLUTION}" \ + --allow-missing-template-keys=false &> /dev/null; then + echo 1>&2 "Solution '$SOLUTION' is not configured in Namespace" \ + "'$NAMESPACE'" + return 1 + fi + + local -ra patch=( + "[{" + " 'op': 'remove'," + " 'path': '/data/$SOLUTION'" + "}]" + ) + + run "Removing Solution '$SOLUTION'" \ + kubectl patch configmap "$ENV_CONFIGMAP" \ + --namespace "$NAMESPACE" \ + --type json \ + --patch "${patch[*]}" + + local -ra pillar=( + "{" + " 'orchestrate': {" + " 'env_name': '$NAME'" + " }" + "}" + ) + + run "Preparing Environment '$NAME'" \ + salt_master_exec salt-run state.orchestrate \ + metalk8s.orchestrate.solutions.prepare-environment \ + pillar="${pillar[*]}" +} + +if ! (( $# )); then + echo 1>&2 "You must provide one of the following commands as the" \ + "first positional argument: ${!COMMANDS[*]}" + usage + exit 1 +fi + +COMMAND=$1 + +if ! [[ ${COMMANDS[$COMMAND]:-} ]]; then + echo 1>&2 "Command '$COMMAND' is invalid, please use one of:" \ + "${!COMMANDS[*]}" + usage + exit 1 +fi + +check_command_mandatory_options "$COMMAND" || exit + +"${COMMANDS[$COMMAND]}"