From 723abae3d3a96decba24795ac7545b0c13a2e93d Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Fri, 11 Oct 2019 08:25:34 +0200 Subject: [PATCH 01/19] salt/solutions: Deploy Solutions K8s dependencies Some objects are required for MetalK8s to manage Solutions, notably a Namespace and ConfigMap for Admin UIs, and the `Environment` CRD. Issue: #1852 --- buildchain/buildchain/salt_tree.py | 6 ++++-- salt/metalk8s/addons/solutions/deployed.sls | 2 -- .../{ => deployed}/environment-crd.sls | 0 .../addons/solutions/deployed/init.sls | 4 ++++ .../addons/solutions/deployed/namespace.sls | 10 ++++++++++ .../solutions/deployed/uis-configmap.sls | 19 +++++++++++++++++++ 6 files changed, 37 insertions(+), 4 deletions(-) delete mode 100644 salt/metalk8s/addons/solutions/deployed.sls rename salt/metalk8s/addons/solutions/{ => deployed}/environment-crd.sls (100%) create mode 100644 salt/metalk8s/addons/solutions/deployed/init.sls create mode 100644 salt/metalk8s/addons/solutions/deployed/namespace.sls create mode 100644 salt/metalk8s/addons/solutions/deployed/uis-configmap.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 0e8c172db2..29a2009c9a 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -235,8 +235,10 @@ 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/init.sls'), + Path('salt/metalk8s/addons/solutions/deployed/environment-crd.sls'), + Path('salt/metalk8s/addons/solutions/deployed/namespace.sls'), + Path('salt/metalk8s/addons/solutions/deployed/uis-configmap.sls'), Path('salt/metalk8s/addons/volumes/deployed.sls'), targets.TemplateFile( 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/environment-crd.sls b/salt/metalk8s/addons/solutions/deployed/environment-crd.sls similarity index 100% rename from salt/metalk8s/addons/solutions/environment-crd.sls rename to salt/metalk8s/addons/solutions/deployed/environment-crd.sls diff --git a/salt/metalk8s/addons/solutions/deployed/init.sls b/salt/metalk8s/addons/solutions/deployed/init.sls new file mode 100644 index 0000000000..09b7112416 --- /dev/null +++ b/salt/metalk8s/addons/solutions/deployed/init.sls @@ -0,0 +1,4 @@ +include: + - .environment-crd + - .namespace + - .uis-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/deployed/uis-configmap.sls b/salt/metalk8s/addons/solutions/deployed/uis-configmap.sls new file mode 100644 index 0000000000..39f970a515 --- /dev/null +++ b/salt/metalk8s/addons/solutions/deployed/uis-configmap.sls @@ -0,0 +1,19 @@ +#!jinja | kubernetes kubeconfig=/etc/kubernetes/admin.conf&context=kubernetes-admin@kubernetes + +{%- from "metalk8s/map.jinja" import repo with context %} + +apiVersion: v1 +kind: ConfigMap +metadata: + name: ui-branding + namespace: metalk8s-solutions +data: + config.json: | + { + "url": "https://{{ pillar.metalk8s.api_server.host }}:6443", + "registry_prefix": "{{ repo.registry_endpoint }}" + } + theme.json: | + { + "brand": {"primary": "#403e40", "secondary": "#e99121"} + } From 88dfbfa9ab8d28817acae152a2d27e97000559cd Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Mon, 14 Oct 2019 23:29:43 +0200 Subject: [PATCH 02/19] salt/solutions: Revamp configuration file A new field, currently named `active` (may be a poor choice, we'll see with first user interactions, `activeVersions` could be more explicit), is introduced to set which version of a Solution should be used for the cluster-wide components of this Solution (i.e. the Admin UI and CRDs for now). Validation of the file format is added, using the usual `kind` and `apiVersion` fields. Later improvements could use some OpenAPI schema. Fixes: #1582 --- salt/_modules/metalk8s_solutions.py | 53 +++++++++++++++++++++++++---- salt/_pillar/metalk8s_solutions.py | 33 ++++++++---------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index 13c8f565b8..ab75068a0d 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -3,14 +3,12 @@ ''' import json import logging + import yaml 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 @@ -20,6 +18,14 @@ log = logging.getLogger(__name__) +SOLUTIONS_CONFIG_MAP = 'metalk8s-solutions' +SOLUTIONS_CONFIG_MAP_NAMESPACE = 'metalk8s-solutions' + +SOLUTIONS_CONFIG_FILE = '/etc/metalk8s/solutions.yaml' +SUPPORTED_CONFIG_VERSIONS = frozenset(( + 'solutions.metalk8s.scality.com/{}'.format(version) + for version in ['v1alpha1'] +)) __virtualname__ = 'metalk8s_solutions' @@ -51,16 +57,49 @@ def list_deployed( } -def list_configured(): - """Get list of Solution archives paths defined in a config file.""" +def read_config(): + """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') + """ try: with open(SOLUTIONS_CONFIG_FILE, 'r') as fd: - content = yaml.safe_load(fd) + config = yaml.safe_load(fd) except Exception as exc: msg = 'Failed to load "{}": {}'.format(SOLUTIONS_CONFIG_FILE, str(exc)) raise CommandExecutionError(message=msg) - return content.get('archives', []) or [] + if config.get('kind') != 'SolutionsConfiguration': + raise CommandExecutionError( + 'Invalid `kind` in configuration ({}), ' + 'must be "SolutionsConfiguration"'.format(config.get('kind')) + ) + + 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 def register_solution_version( diff --git a/salt/_pillar/metalk8s_solutions.py b/salt/_pillar/metalk8s_solutions.py index 8ebb1b15de..fbb293e7df 100644 --- a/salt/_pillar/metalk8s_solutions.py +++ b/salt/_pillar/metalk8s_solutions.py @@ -7,41 +7,36 @@ def __virtual__(): + if 'metalk8s_solutions.read_config' not in __salt__: + return False, "Failed to load 'metalk8s_solution' module." return __virtualname__ def _load_solutions(): """Load Solutions from ConfigMap and config file.""" - errors = [] try: - deployed = __salt__['metalk8s_solutions.list_deployed']() - except KeyError: - return __utils__['pillar_utils.errors_to_dict']([ - "Failed to load 'metalk8s_solutions' module." + config_data = __salt__['metalk8s_solutions.read_config']() + except (IOError, CommandExecutionError) as exc: + config_data = __utils__['pillar_utils.errors_to_dict']([ + "Error when reading Solutions config file: {}".format(exc) ]) + + try: + deployed = __salt__['metalk8s_solutions.list_deployed']() except Exception as exc: - deployed = {} - errors.append( + deployed = __utils__['pillar_utils.errors_to_dict']([ "Error when retrieving ConfigMap 'metalk8s-solutions': {}".format( exc ) - ) - - try: - configured = __salt__['metalk8s_solutions.list_configured']() - except (IOError, CommandExecutionError) as exc: - configured = [] - errors.append( - "Error when reading Solutions config file: {}".format(exc) - ) + ]) result = { - 'configured': configured, + 'config': config_data, 'deployed': deployed, } - if errors: - result.update(__utils__['pillar_utils.errors_to_dict'](errors)) + for key in result: + __utils__['pillar_utils.promote_errors'](result, key) return result From 70feeab607c8129a89343164430eea23a17bae0f Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Tue, 15 Oct 2019 00:03:28 +0200 Subject: [PATCH 03/19] salt/solutions: Revamp current state in pillar Previous approached relied on a ConfigMap, which poses numerous issues: - it allows one to lie to the formula, potentially causing further problems down the line - the CM cannot always be up-to-date with the actual system state (due to errors when updating it, or manual operations outside of Salt) We change this approach by including basic heuristics in the `metalk8s_solutions` module, which can be refined in the future. Listing of mounted Solutions is made based on `mount.available` and a naive filtering on the mountpoints (necessarily under /srv/scality). Listing of active Solution versions is made by looking for UI Service objects in the `metalk8s-solutions` namespace. This approach relies on labels that we don't set ourselves, which is brittle, and does not account for Solutions that may want to ship without an Admin UI. Future iterations could improve those methods, but we are convinced it is still safer than the ConfigMap method used before. Issue: #1852 --- buildchain/buildchain/salt_tree.py | 10 +- salt/_modules/metalk8s_solutions.py | 199 ++++++------------------ salt/_modules/metalk8s_solutions_k8s.py | 71 +++++++++ salt/_pillar/metalk8s_solutions.py | 76 +++++++-- salt/metalk8s/repo/installed.sls | 2 +- 5 files changed, 185 insertions(+), 173 deletions(-) create mode 100644 salt/_modules/metalk8s_solutions_k8s.py diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 29a2009c9a..6f9b8c0506 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -493,26 +493,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/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index ab75068a0d..29aff88bc0 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -1,26 +1,16 @@ -''' -Various utilities to manage Solutions. -''' -import json -import logging +"""Utility methods for Solutions management. -import yaml +This module contains minion-local operations, see `metalk8s_solutions_k8s.py` +for the K8s operations in the virtual `metalk8s_solutions` module. +""" +import collections +import logging from salt.exceptions import CommandExecutionError - -HAS_LIBS = True -try: - import kubernetes.client - from kubernetes.client.rest import ApiException - from urllib3.exceptions import HTTPError -except ImportError: - HAS_LIBS = False +import yaml log = logging.getLogger(__name__) -SOLUTIONS_CONFIG_MAP = 'metalk8s-solutions' -SOLUTIONS_CONFIG_MAP_NAMESPACE = 'metalk8s-solutions' - SOLUTIONS_CONFIG_FILE = '/etc/metalk8s/solutions.yaml' SUPPORTED_CONFIG_VERSIONS = frozenset(( 'solutions.metalk8s.scality.com/{}'.format(version) @@ -31,30 +21,9 @@ 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 {} - - return { - name: json.loads(versions_str) - for name, versions_str in response_dict['data'].items() - } + if 'metalk8s.archive_info_from_iso' not in __salt__: + return False, "Failed to load 'metalk8s' module." + return __virtualname__ def read_config(): @@ -102,124 +71,50 @@ def read_config(): return config -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 _is_solution_mount(mount_tuple): + """Return whether a mount is for a Solution archive. - all_versions_str = (configmap.get('data') or {}).get(name, '[]') - all_versions = json.loads(all_versions_str) + Any ISO9660 mounted in `/srv/scality` that isn't for MetalK8s is considered + to be a Solution archive. + """ + mountpoint, mount_info = mount_tuple - for version_dict in all_versions: - if version_dict['version'] == version: - version_dict['iso'] = archive_path - version_dict['deployed'] = deployed - break - else: - all_versions.append({ - 'version': version, - 'iso': archive_path, - 'deployed': deployed, - }) + if not mountpoint.startswith('/srv/scality/'): + return False - body = {'data': {name: json.dumps(all_versions)}} + if mountpoint.startswith('/srv/scality/metalk8s-'): + return False - 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, exc) + if mount_info['fstype'] != 'iso9660': return False 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 - ) - 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 +def list_available(): + """Get a view of mounted Solution archives. - return True + 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({ + 'display_name': name, + 'machine_id': '{}-{}'.format(machine_name, version), + 'mountpoint': mountpoint, + 'archive': mount_info['alt_device'], + 'version': 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..3c1e9dd300 --- /dev/null +++ b/salt/_modules/metalk8s_solutions_k8s.py @@ -0,0 +1,71 @@ +"""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 logging + +from salt.exceptions import CommandExecutionError + +log = logging.getLogger(__name__) + +__virtualname__ = 'metalk8s_solutions' + +SOLUTIONS_NAMESPACE = 'metalk8s-solutions' + + +def __virtual__(): + if 'metalk8s_kubernetes.services' not in __salt__: + return False, "Failed to load 'metalk8s_kubernetes' module" + return __virtualname__ + + +def list_active( + context="kubernetes-admin@kubernetes", + kubeconfig="/etc/kubernetes/admin.conf", +): + """List all Solution versions for which components are deployed. + + Currently only checks Admin UIs `Service` objects, using labels to + determine if these objects are actually what we think they are. + FIXME: this approach can be brittle. + """ + all_service_names = __salt__['metalk8s_kubernetes.services']( + namespace=SOLUTIONS_NAMESPACE, + context=context, + kubeconfig=kubeconfig, + ) + + result = {} + for service_name in all_service_names: + # FIXME: get rid of this stupidity, we should not need multiple calls + service = __salt__['metalk8s_kubernetes.show_service']( + name=service_name, + namespace=SOLUTIONS_NAMESPACE, + context=context, + kubeconfig=kubeconfig, + ) + labels = service.get('metadata', {}).get('labels', {}) + + if labels.get("app.kubernetes.io/component") != "ui": + # Not an Admin UI, ignoring for this list + continue + + try: + solution_name = labels["app.kubernetes.io/part-of"] + solution_version = labels["app.kubernetes.io/version"] + except KeyError: + log.warn("Ignoring UI Service '%s' due to missing labels.", + service_name) + continue + + if solution_name in result: + raise CommandExecutionError( + "Found multiple UI Services in '{}' namespace belonging to " + "the same Solution. Only one Admin UI per Solution is " + "supported.".format(SOLUTIONS_NAMESPACE) + ) + + result[solution_name] = solution_version + + return result diff --git a/salt/_pillar/metalk8s_solutions.py b/salt/_pillar/metalk8s_solutions.py index fbb293e7df..71d90015ed 100644 --- a/salt/_pillar/metalk8s_solutions.py +++ b/salt/_pillar/metalk8s_solutions.py @@ -12,34 +12,80 @@ def __virtual__(): return __virtualname__ -def _load_solutions(): +def _load_solutions(bootstrap_id): """Load Solutions from ConfigMap and config file.""" + result = { + 'available': {}, + 'config': {}, + } + try: - config_data = __salt__['metalk8s_solutions.read_config']() + result['config'] = __salt__['metalk8s_solutions.read_config']() except (IOError, CommandExecutionError) as exc: - config_data = __utils__['pillar_utils.errors_to_dict']([ + result['config'] = __utils__['pillar_utils.errors_to_dict']([ "Error when reading Solutions config file: {}".format(exc) ]) + errors = [] try: - deployed = __salt__['metalk8s_solutions.list_deployed']() + result['available'] = __salt__['saltutil.cmd']( + tgt=bootstrap_id, + fun='metalk8s_solutions.list_available', + )[bootstrap_id]['ret'] except Exception as exc: - deployed = __utils__['pillar_utils.errors_to_dict']([ - "Error when retrieving ConfigMap 'metalk8s-solutions': {}".format( - exc - ) - ]) + errors.append( + "Error when listing available Solutions: {}".format(exc) + ) - result = { - 'config': config_data, - 'deployed': deployed, - } + try: + active = __salt__['metalk8s_solutions.list_active']() + except Exception as exc: + errors.append( + "Error when listing active Solution versions: {}".format(exc) + ) + + if errors: + result['available'].update( + __utils__['pillar_utils.errors_to_dict'](errors) + ) + return result + + # 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 - for key in result: + for key in ['available', 'config']: __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/repo/installed.sls b/salt/metalk8s/repo/installed.sls index 3b939b4dca..1a2070f4d4 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' %} From 883d1fdb71131625b5412936e736087c9b24cbc2 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 10 Oct 2019 09:49:11 +0200 Subject: [PATCH 04/19] salt/solutions: Revamp state for available ISOs Making a Solution archive "available" to the cluster was split into two formulas: `metalk8s.solutions.mounted` and `.configured`. We group them into a single one, called `.available`. We also remove the `.unmounted` and `.unconfigured` formulas, since the source of truth is entirely held by the configuration file (/etc/metalk8s/solutions.yaml). A single formula is responsible for applying the desired state described in this configuration, adding and removing Solution archives from the cluster. --- buildchain/buildchain/salt_tree.py | 9 +- salt/metalk8s/solutions/available.sls | 120 +++++++++++++++++++++++ salt/metalk8s/solutions/configured.sls | 24 ----- salt/metalk8s/solutions/init.sls | 31 ++++++ salt/metalk8s/solutions/mounted.sls | 54 ---------- salt/metalk8s/solutions/unconfigured.sls | 26 ----- salt/metalk8s/solutions/unmounted.sls | 34 ------- 7 files changed, 154 insertions(+), 144 deletions(-) create mode 100644 salt/metalk8s/solutions/available.sls delete mode 100644 salt/metalk8s/solutions/configured.sls delete mode 100644 salt/metalk8s/solutions/mounted.sls delete mode 100644 salt/metalk8s/solutions/unconfigured.sls delete mode 100644 salt/metalk8s/solutions/unmounted.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 6f9b8c0506..5ac3ac6940 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -433,12 +433,6 @@ def _get_parts(self) -> Iterator[str]: 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'), @@ -483,6 +477,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'), diff --git a/salt/metalk8s/solutions/available.sls b/salt/metalk8s/solutions/available.sls new file mode 100644 index 0000000000..ca5857c58c --- /dev/null +++ b/salt/metalk8s/solutions/available.sls @@ -0,0 +1,120 @@ +# +# 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 %} + +include: + - metalk8s.repo.installed + +{%- macro extract_info(archive_path) %} + + {{ machine_name }},{{ display_name }},{{ mount_path }} +{%- endmacro %} + +{%- 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('; ') }}" + +{%- else %} + {#- Mount configured #} + {%- set configured = pillar.metalk8s.solutions.config.archives %} + {%- for archive_path in configured %} + {%- set solution = salt['metalk8s.archive_info_from_iso'](archive_path) %} + {%- set lower_name = solution.name | replace(' ', '-') | lower %} + {%- set machine_name = lower_name ~ '-' ~ solution.version %} + {%- set display_name = solution.name ~ ' ' ~ solution.version %} + {%- set mount_path = "/srv/scality/" ~ machine_name -%} + +{# 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 }}/{{ machine_name }}-registry-config.inc + - template: jinja + - defaults: + repository: {{ machine_name }} + registry_root: {{ mount_path }}/images + - require: + - file: Container images for Solution {{ display_name }} exist + - require_in: + - sls: metalk8s.repo.installed + + {%- endfor %} {# Configured solutions are all mounted and images exposed #} + + {#- Unmount all Solution ISOs mounted in /srv/scality not referenced in + the configuration file #} + {%- set available = pillar.metalk8s.solutions.available %} + {%- for name, versions in available.items() %} + {%- for info in versions %} + {%- if info.archive not in configured %} + {%- set display_name = info.display_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 }}/{{ name }}-registry-config.inc + - require_in: + - sls: metalk8s.repo.installed + +Unmount Solution {{ display_name }}: + mount.unmounted: + - name: {{ info.mountpoint }} + - device: {{ info.archive }} + - 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 From 67bcf7cb7d51ab997670f5f9d7a56871089ef7e0 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Tue, 15 Oct 2019 00:30:19 +0200 Subject: [PATCH 05/19] salt/solutions: Revamp orchestrate for K8s objects Once a Solution is "available" (i.e. mounted and its images exposed by the cluster registry), its cluster-wide components can be deployed. We rely on the `active` mapping of active versions in the config file to know which components to deploy or remove. Note that one can set a desired version to 'latest' in this mapping, which the orchestration will handle for using the highest version available for a Solution (in SemVer terms). Issue: #1852 --- buildchain/buildchain/salt_tree.py | 3 +- .../orchestrate/solutions/available.sls | 42 --- .../solutions/deploy-components.sls | 160 +++++++++++ salt/metalk8s/orchestrate/solutions/init.sls | 253 ------------------ salt/metalk8s/solutions/available.sls | 1 - 5 files changed, 161 insertions(+), 298 deletions(-) delete mode 100644 salt/metalk8s/orchestrate/solutions/available.sls create mode 100644 salt/metalk8s/orchestrate/solutions/deploy-components.sls delete mode 100644 salt/metalk8s/orchestrate/solutions/init.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 5ac3ac6940..347401cde5 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -421,8 +421,7 @@ def _get_parts(self) -> Iterator[str]: 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/solutions/deploy-components.sls'), Path('salt/metalk8s/orchestrate/etcd.sls'), Path('salt/metalk8s/orchestrate/upgrade/init.sls'), Path('salt/metalk8s/orchestrate/upgrade/precheck.sls'), 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..a062703ca1 --- /dev/null +++ b/salt/metalk8s/orchestrate/solutions/deploy-components.sls @@ -0,0 +1,160 @@ +{%- from "metalk8s/map.jinja" import repo with context %} + +include: + - metalk8s.addons.solutions.deployed.namespace + +{%- 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 %} + + {# Admin UI management #} + {%- for ui_file in ui_files %} + {%- set filepath = salt.file.join(solution.mountpoint, ui_relpath, ui_file) %} + {%- set repository = repo.registry_endpoint ~ "/" ~ solution.machine_id %} + {%- set sls_content = salt.saltutil.cmd( + tgt=pillar.bootstrap_id, + fun='slsutil.renderer', + kwarg={ + 'path': filepath, + 'default_renderer': renderer, + 'repository': repository, + }, + )[pillar.bootstrap_id]['ret'] + %} +{{ action }} Admin UI "{{ ui_file }}" for Solution {{ solution.display_name }}: + module.run: + - state.template_str: + - tem: "{{ sls_content | yaml }}" + - require: + - sls: metalk8s.addons.solutions.deployed.namespace + + {%- endfor %} {# ui_file in ui_files #} + + {# CRDs management #} + {%- 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.display_name }}: + module.run: + - state.template_str: + - tem: "{{ sls_content | yaml }}" + + {%- endfor %} {# crd_file in crd_files #} +{%- 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 %} + {%- endfor %} + + {%- for name in desired | difference(available) %} + {{- fail_missing_solution(name) }} + {%- endfor %} + +{%- endif %} {# _errors in pillar #} 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/solutions/available.sls b/salt/metalk8s/solutions/available.sls index ca5857c58c..00f4ed88e7 100644 --- a/salt/metalk8s/solutions/available.sls +++ b/salt/metalk8s/solutions/available.sls @@ -10,7 +10,6 @@ include: - metalk8s.repo.installed {%- macro extract_info(archive_path) %} - {{ machine_name }},{{ display_name }},{{ mount_path }} {%- endmacro %} From 5d9ea9b1bb0b937dd76a725ae13520f1dd006a5e Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Wed, 16 Oct 2019 10:02:04 +0200 Subject: [PATCH 06/19] salt/solutions: Rename some attributes in pillar We used `display_name` and `machine_id` to differenciate the expected consumers (user or machine). For simplicity (thanks to slaperche-scality), we rename them in `name` and `id` respectively. KISS! --- salt/_modules/metalk8s_solutions.py | 4 ++-- .../orchestrate/solutions/deploy-components.sls | 6 +++--- .../repo/files/repositories-manifest.yaml.j2 | 12 ++++++------ salt/metalk8s/repo/installed.sls | 4 ++-- salt/metalk8s/solutions/available.sls | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index 29aff88bc0..f04449f213 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -110,8 +110,8 @@ def list_available(): version = solution_info['version'] result[machine_name].append({ - 'display_name': name, - 'machine_id': '{}-{}'.format(machine_name, version), + 'name': name, + 'id': '{}-{}'.format(machine_name, version), 'mountpoint': mountpoint, 'archive': mount_info['alt_device'], 'version': version, diff --git a/salt/metalk8s/orchestrate/solutions/deploy-components.sls b/salt/metalk8s/orchestrate/solutions/deploy-components.sls index a062703ca1..137cb903dc 100644 --- a/salt/metalk8s/orchestrate/solutions/deploy-components.sls +++ b/salt/metalk8s/orchestrate/solutions/deploy-components.sls @@ -25,7 +25,7 @@ include: {# Admin UI management #} {%- for ui_file in ui_files %} {%- set filepath = salt.file.join(solution.mountpoint, ui_relpath, ui_file) %} - {%- set repository = repo.registry_endpoint ~ "/" ~ solution.machine_id %} + {%- set repository = repo.registry_endpoint ~ "/" ~ solution.id %} {%- set sls_content = salt.saltutil.cmd( tgt=pillar.bootstrap_id, fun='slsutil.renderer', @@ -36,7 +36,7 @@ include: }, )[pillar.bootstrap_id]['ret'] %} -{{ action }} Admin UI "{{ ui_file }}" for Solution {{ solution.display_name }}: +{{ action }} Admin UI "{{ ui_file }}" for Solution {{ solution.name }}: module.run: - state.template_str: - tem: "{{ sls_content | yaml }}" @@ -65,7 +65,7 @@ include: 'default_renderer': renderer, }, )[pillar.bootstrap_id]['ret'] %} -{{ action }} CRD "{{ crd_file }}" for Solution {{ solution.display_name }}: +{{ action }} CRD "{{ crd_file }}" for Solution {{ solution.name }}: module.run: - state.template_str: - tem: "{{ sls_content | yaml }}" 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 1a2070f4d4..3a9957bb11 100644 --- a/salt/metalk8s/repo/installed.sls +++ b/salt/metalk8s/repo/installed.sls @@ -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 index 00f4ed88e7..b031666257 100644 --- a/salt/metalk8s/solutions/available.sls +++ b/salt/metalk8s/solutions/available.sls @@ -23,10 +23,10 @@ Cannot proceed with mounting of Solution archives: {%- set configured = pillar.metalk8s.solutions.config.archives %} {%- for archive_path in configured %} {%- set solution = salt['metalk8s.archive_info_from_iso'](archive_path) %} - {%- set lower_name = solution.name | replace(' ', '-') | lower %} - {%- set machine_name = lower_name ~ '-' ~ solution.version %} + {%- 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/" ~ machine_name -%} + {%- set mount_path = "/srv/scality/" ~ id -%} {# Mount the archive #} Mountpoint for Solution {{ display_name }} exists: @@ -67,10 +67,10 @@ Container images for Solution {{ display_name }} exist: Expose container images for Solution {{ display_name }}: file.managed: - source: {{ mount_path }}/registry-config.inc.j2 - - name: {{ repo.config.directory }}/{{ machine_name }}-registry-config.inc + - name: {{ repo.config.directory }}/{{ id }}-registry-config.inc - template: jinja - defaults: - repository: {{ machine_name }} + repository: {{ id }} registry_root: {{ mount_path }}/images - require: - file: Container images for Solution {{ display_name }} exist @@ -82,10 +82,10 @@ Expose container images for Solution {{ display_name }}: {#- Unmount all Solution ISOs mounted in /srv/scality not referenced in the configuration file #} {%- set available = pillar.metalk8s.solutions.available %} - {%- for name, versions in available.items() %} + {%- for machine_name, versions in available.items() %} {%- for info in versions %} {%- if info.archive not in configured %} - {%- set display_name = info.display_name ~ ' ' ~ info.version %} + {%- set display_name = info.name ~ ' ' ~ info.version %} {%- if info.active %} Cannot remove archive for active Solution {{ display_name }}: test.fail_without_changes: @@ -94,7 +94,7 @@ Cannot remove archive for active Solution {{ display_name }}: {%- else %} Remove container images for Solution {{ display_name }}: file.absent: - - name: {{ repo.config.directory }}/{{ name }}-registry-config.inc + - name: {{ repo.config.directory }}/{{ info.id }}-registry-config.inc - require_in: - sls: metalk8s.repo.installed From 1d57d9994a1318f9c5013972269aa43ddec8f62f Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Fri, 18 Oct 2019 16:12:23 +0200 Subject: [PATCH 07/19] salt/solutions: Manage config file from Salt Use Salt to read/write the Solutions config file --- salt/_modules/metalk8s_solutions.py | 102 +++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index f04449f213..f91b4d6c21 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -4,6 +4,7 @@ for the K8s operations in the virtual `metalk8s_solutions` module. """ import collections +import errno import logging from salt.exceptions import CommandExecutionError @@ -11,11 +12,12 @@ log = logging.getLogger(__name__) -SOLUTIONS_CONFIG_FILE = '/etc/metalk8s/solutions.yaml' -SUPPORTED_CONFIG_VERSIONS = frozenset(( +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' @@ -26,7 +28,40 @@ def __virtual__(): return __virtualname__ -def read_config(): +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 _write_config_file(data): + try: + with open(CONFIG_FILE, 'w') as fd: + yaml.safe_dump(data, fd) + except Exception as exc: + msg = 'Failed to write Solutions config file at "{}": {}'.format( + CONFIG_FILE, exc + ) + raise CommandExecutionError(message=msg) + + +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 @@ -42,13 +77,11 @@ def read_config(): - /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. """ - try: - with open(SOLUTIONS_CONFIG_FILE, 'r') as fd: - config = yaml.safe_load(fd) - except Exception as exc: - msg = 'Failed to load "{}": {}'.format(SOLUTIONS_CONFIG_FILE, str(exc)) - raise CommandExecutionError(message=msg) + config = _load_config_file(create=create) if config.get('kind') != 'SolutionsConfiguration': raise CommandExecutionError( @@ -71,6 +104,55 @@ def read_config(): return config +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: + if archive not in config['archives']: + config['archives'].append(archive) + + _write_config_file(config) + return True + + +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) + ) + + # 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 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. From 6e1fd64bcde90255178c0338d4faf00344ab2922 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Wed, 13 Nov 2019 10:23:58 +0100 Subject: [PATCH 08/19] salt: remove Environment CRD Environment CRD is not longer needed as we use directly a ConfigMap in each namespace --- buildchain/buildchain/salt_tree.py | 1 - .../solutions/deployed/environment-crd.sls | 66 ------------------- .../addons/solutions/deployed/init.sls | 1 - 3 files changed, 68 deletions(-) delete mode 100644 salt/metalk8s/addons/solutions/deployed/environment-crd.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 347401cde5..88c18d5f8b 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -236,7 +236,6 @@ def _get_parts(self) -> Iterator[str]: Path('salt/metalk8s/addons/ui/deployed/ui.sls'), Path('salt/metalk8s/addons/solutions/deployed/init.sls'), - Path('salt/metalk8s/addons/solutions/deployed/environment-crd.sls'), Path('salt/metalk8s/addons/solutions/deployed/namespace.sls'), Path('salt/metalk8s/addons/solutions/deployed/uis-configmap.sls'), diff --git a/salt/metalk8s/addons/solutions/deployed/environment-crd.sls b/salt/metalk8s/addons/solutions/deployed/environment-crd.sls deleted file mode 100644 index ac393dd871..0000000000 --- a/salt/metalk8s/addons/solutions/deployed/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/addons/solutions/deployed/init.sls b/salt/metalk8s/addons/solutions/deployed/init.sls index 09b7112416..34866d908e 100644 --- a/salt/metalk8s/addons/solutions/deployed/init.sls +++ b/salt/metalk8s/addons/solutions/deployed/init.sls @@ -1,4 +1,3 @@ include: - - .environment-crd - .namespace - .uis-configmap From b77574b8080db791f4e4c99a4ea947eec4d4784e Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 14 Nov 2019 23:04:11 +0100 Subject: [PATCH 09/19] salt/solutions: environments in Pillar Add salt module to read solution environment ConfigMap and retrieve it in the solution ext_pillar --- salt/_modules/metalk8s_solutions_k8s.py | 46 +++++++++++++++++++++++++ salt/_pillar/metalk8s_solutions.py | 23 +++++++++---- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/salt/_modules/metalk8s_solutions_k8s.py b/salt/_modules/metalk8s_solutions_k8s.py index 3c1e9dd300..cc209d4725 100644 --- a/salt/_modules/metalk8s_solutions_k8s.py +++ b/salt/_modules/metalk8s_solutions_k8s.py @@ -12,6 +12,10 @@ __virtualname__ = 'metalk8s_solutions' SOLUTIONS_NAMESPACE = '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__(): @@ -69,3 +73,45 @@ def list_active( result[solution_name] = solution_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 71d90015ed..a96d45cce9 100644 --- a/salt/_pillar/metalk8s_solutions.py +++ b/salt/_pillar/metalk8s_solutions.py @@ -17,6 +17,7 @@ def _load_solutions(bootstrap_id): result = { 'available': {}, 'config': {}, + 'environments': {}, } try: @@ -48,15 +49,23 @@ def _load_solutions(bootstrap_id): result['available'].update( __utils__['pillar_utils.errors_to_dict'](errors) ) - return result + 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 - # 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']: + for key in ['available', 'config', 'environments']: __utils__['pillar_utils.promote_errors'](result, key) return result From b38b5f759796cdbdae06064e74d8002181e08c34 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 14 Nov 2019 23:12:10 +0100 Subject: [PATCH 10/19] salt/solution: Reinstate ConfigMap We re-instate the ConfigMap as both a way to track which version was made available (through the `deploy-components` orchestrate) and a way to share the information with MetalK8s UI users. --- buildchain/buildchain/salt_tree.py | 1 + salt/_modules/metalk8s_solutions_k8s.py | 64 +++++++------------ .../addons/solutions/deployed/configmap.sls | 7 ++ .../addons/solutions/deployed/init.sls | 1 + .../solutions/deploy-components.sls | 26 +++++++- 5 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 salt/metalk8s/addons/solutions/deployed/configmap.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 88c18d5f8b..bb8b47e947 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -235,6 +235,7 @@ 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/configmap.sls'), Path('salt/metalk8s/addons/solutions/deployed/init.sls'), Path('salt/metalk8s/addons/solutions/deployed/namespace.sls'), Path('salt/metalk8s/addons/solutions/deployed/uis-configmap.sls'), diff --git a/salt/_modules/metalk8s_solutions_k8s.py b/salt/_modules/metalk8s_solutions_k8s.py index cc209d4725..b27478d58b 100644 --- a/salt/_modules/metalk8s_solutions_k8s.py +++ b/salt/_modules/metalk8s_solutions_k8s.py @@ -3,6 +3,7 @@ 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 @@ -12,6 +13,7 @@ __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' @@ -19,58 +21,36 @@ def __virtual__(): - if 'metalk8s_kubernetes.services' not in __salt__: - return False, "Failed to load 'metalk8s_kubernetes' module" + # TODO: consider checking methods from metalk8s_kubernetes return __virtualname__ -def list_active( - context="kubernetes-admin@kubernetes", - kubeconfig="/etc/kubernetes/admin.conf", -): +def list_active(**kwargs): """List all Solution versions for which components are deployed. - Currently only checks Admin UIs `Service` objects, using labels to - determine if these objects are actually what we think they are. - FIXME: this approach can be brittle. + Currently relies on the ConfigMap that is managed in the + `deploy-components` orchestration. """ - all_service_names = __salt__['metalk8s_kubernetes.services']( + solutions_config = __salt__['metalk8s_kubernetes.get_object']( + kind='ConfigMap', + apiVersion='v1', + name=SOLUTIONS_CONFIGMAP_NAME, namespace=SOLUTIONS_NAMESPACE, - context=context, - kubeconfig=kubeconfig, + **kwargs ) - result = {} - for service_name in all_service_names: - # FIXME: get rid of this stupidity, we should not need multiple calls - service = __salt__['metalk8s_kubernetes.show_service']( - name=service_name, - namespace=SOLUTIONS_NAMESPACE, - context=context, - kubeconfig=kubeconfig, + + 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 ) - labels = service.get('metadata', {}).get('labels', {}) - - if labels.get("app.kubernetes.io/component") != "ui": - # Not an Admin UI, ignoring for this list - continue - - try: - solution_name = labels["app.kubernetes.io/part-of"] - solution_version = labels["app.kubernetes.io/version"] - except KeyError: - log.warn("Ignoring UI Service '%s' due to missing labels.", - service_name) - continue - - if solution_name in result: - raise CommandExecutionError( - "Found multiple UI Services in '{}' namespace belonging to " - "the same Solution. Only one Admin UI per Solution is " - "supported.".format(SOLUTIONS_NAMESPACE) - ) - - result[solution_name] = solution_version + if active_version is not None: + result[name] = active_version return result 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 index 34866d908e..582be69fcb 100644 --- a/salt/metalk8s/addons/solutions/deployed/init.sls +++ b/salt/metalk8s/addons/solutions/deployed/init.sls @@ -1,3 +1,4 @@ include: - .namespace - .uis-configmap + - .configmap diff --git a/salt/metalk8s/orchestrate/solutions/deploy-components.sls b/salt/metalk8s/orchestrate/solutions/deploy-components.sls index 137cb903dc..671174498b 100644 --- a/salt/metalk8s/orchestrate/solutions/deploy-components.sls +++ b/salt/metalk8s/orchestrate/solutions/deploy-components.sls @@ -1,7 +1,7 @@ {%- from "metalk8s/map.jinja" import repo with context %} include: - - metalk8s.addons.solutions.deployed.namespace + - metalk8s.addons.solutions.deployed {%- set kubernetes_present_renderer = "jinja | metalk8s_kubernetes" %} {%- set kubernetes_absent_renderer = @@ -151,6 +151,30 @@ Cannot proceed with deployment of Solution cluster-wide components: {{- 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) %} From 23581b1cbcc51d3bd2071fcc7f7cb18799584136 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 14 Nov 2019 23:15:14 +0100 Subject: [PATCH 11/19] salt/solution: Stop deploying Admin UI cluster-wide The setup of "environment" Namespaces and all the resources required to run a Solution instance in it is becoming too complex for it to be properly managed by the Solution Admin UIs, without mentioning the authz requirements on users of said Admin UIs to make it work. Instead, Admin UIs will now be deployed once per environment, making it easier to keep in sync with the same Operator version. --- buildchain/buildchain/salt_tree.py | 1 - .../addons/solutions/deployed/init.sls | 1 - .../solutions/deployed/uis-configmap.sls | 19 ------------- .../solutions/deploy-components.sls | 28 +++---------------- 4 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 salt/metalk8s/addons/solutions/deployed/uis-configmap.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index bb8b47e947..77fcadb368 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -238,7 +238,6 @@ def _get_parts(self) -> Iterator[str]: 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/solutions/deployed/uis-configmap.sls'), Path('salt/metalk8s/addons/volumes/deployed.sls'), targets.TemplateFile( diff --git a/salt/metalk8s/addons/solutions/deployed/init.sls b/salt/metalk8s/addons/solutions/deployed/init.sls index 582be69fcb..64bc08233a 100644 --- a/salt/metalk8s/addons/solutions/deployed/init.sls +++ b/salt/metalk8s/addons/solutions/deployed/init.sls @@ -1,4 +1,3 @@ include: - .namespace - - .uis-configmap - .configmap diff --git a/salt/metalk8s/addons/solutions/deployed/uis-configmap.sls b/salt/metalk8s/addons/solutions/deployed/uis-configmap.sls deleted file mode 100644 index 39f970a515..0000000000 --- a/salt/metalk8s/addons/solutions/deployed/uis-configmap.sls +++ /dev/null @@ -1,19 +0,0 @@ -#!jinja | kubernetes kubeconfig=/etc/kubernetes/admin.conf&context=kubernetes-admin@kubernetes - -{%- from "metalk8s/map.jinja" import repo with context %} - -apiVersion: v1 -kind: ConfigMap -metadata: - name: ui-branding - namespace: metalk8s-solutions -data: - config.json: | - { - "url": "https://{{ pillar.metalk8s.api_server.host }}:6443", - "registry_prefix": "{{ repo.registry_endpoint }}" - } - theme.json: | - { - "brand": {"primary": "#403e40", "secondary": "#e99121"} - } diff --git a/salt/metalk8s/orchestrate/solutions/deploy-components.sls b/salt/metalk8s/orchestrate/solutions/deploy-components.sls index 671174498b..517fa63eac 100644 --- a/salt/metalk8s/orchestrate/solutions/deploy-components.sls +++ b/salt/metalk8s/orchestrate/solutions/deploy-components.sls @@ -22,30 +22,8 @@ include: {%- set renderer = kubernetes_absent_renderer %} {%- endif %} - {# Admin UI management #} - {%- for ui_file in ui_files %} - {%- set filepath = salt.file.join(solution.mountpoint, ui_relpath, ui_file) %} - {%- set repository = repo.registry_endpoint ~ "/" ~ solution.id %} - {%- set sls_content = salt.saltutil.cmd( - tgt=pillar.bootstrap_id, - fun='slsutil.renderer', - kwarg={ - 'path': filepath, - 'default_renderer': renderer, - 'repository': repository, - }, - )[pillar.bootstrap_id]['ret'] - %} -{{ action }} Admin UI "{{ ui_file }}" for Solution {{ solution.name }}: - module.run: - - state.template_str: - - tem: "{{ sls_content | yaml }}" - - require: - - sls: metalk8s.addons.solutions.deployed.namespace - - {%- endfor %} {# ui_file in ui_files #} - {# 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, @@ -70,7 +48,9 @@ include: - state.template_str: - tem: "{{ sls_content | yaml }}" - {%- endfor %} {# crd_file in crd_files #} + {%- endfor %} {# crd_file in crd_files #} + + {# TODO: StorageClasses, Grafana dashboards, ... #} {%- endmacro %} {# TODO: this can be improved using the state module from #1713 #} From 3ec19f29997a61f1aaab8adb416e7ea7a544cbdf Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 14 Nov 2019 23:21:39 +0100 Subject: [PATCH 12/19] salt/solution: Support SolutionConfig files Solution ISOs can now contain a `config.yaml` file to describe the few specifics of its Operator and UI. We expose this information in the `metalk8s:solutions:available` pillar, for later consumption in orchestrations. --- salt/_modules/metalk8s_solutions.py | 58 ++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/salt/_modules/metalk8s_solutions.py b/salt/_modules/metalk8s_solutions.py index f91b4d6c21..dadc2a15ba 100644 --- a/salt/_modules/metalk8s_solutions.py +++ b/salt/_modules/metalk8s_solutions.py @@ -5,10 +5,12 @@ """ import collections import errno +import os import logging +import yaml +import salt from salt.exceptions import CommandExecutionError -import yaml log = logging.getLogger(__name__) @@ -173,6 +175,59 @@ def _is_solution_mount(mount_tuple): 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. @@ -197,6 +252,7 @@ def list_available(): 'mountpoint': mountpoint, 'archive': mount_info['alt_device'], 'version': version, + 'config': read_solution_config(mountpoint, machine_name, version), }) return dict(result) From 6c9d332c461b1e81b9e9439184196d03497a6a92 Mon Sep 17 00:00:00 2001 From: Guillaume Demonet Date: Thu, 14 Nov 2019 23:24:31 +0100 Subject: [PATCH 13/19] salt/solution: Prepare environments Salt is now able to "prepare" environments, based on two sources of information: - the environment *Namespace*s (denoted by some well-known labels) and their 'metalk8s-environment' *ConfigMap*s (which lists the Solution versions to deploy in this environment) - the *SolutionConfig*s stored in each Solution ISO which allows Salt to know what should be deployed Both sources of information are stored in the pillar. --- buildchain/buildchain/salt_tree.py | 22 ++- .../solutions/files/operator/configmap.yaml | 14 ++ .../solutions/files/operator/deployment.yaml | 41 ++++ .../solutions/files/operator/role.yaml | 50 +++++ .../files/operator/role_binding.yaml | 12 ++ .../files/operator/service_account.yaml | 5 + .../solutions/files/ui/configmap.yaml | 10 + .../solutions/files/ui/deployment.yaml | 67 +++++++ .../solutions/files/ui/ingress.yaml | 23 +++ .../solutions/files/ui/service.yaml | 21 +++ .../solutions/prepare-environment.sls | 178 ++++++++++++++++++ 11 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 salt/metalk8s/orchestrate/solutions/files/operator/configmap.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/operator/deployment.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/operator/role.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/operator/role_binding.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/operator/service_account.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/ui/configmap.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/ui/deployment.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/ui/ingress.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/files/ui/service.yaml create mode 100644 salt/metalk8s/orchestrate/solutions/prepare-environment.sls diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index 77fcadb368..2b3acfb122 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -414,18 +414,32 @@ 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/deploy-components.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'), 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/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 %} From 64905343419590df60968c340980c324eebab528 Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Mon, 2 Mar 2020 17:45:33 +0100 Subject: [PATCH 14/19] scripts: shorten solution-manager script name Let's be consistent with the script's names, (we don't have bootstrap-manager, downgrade-manager, ...) the "-manager" suffix is not really useful anyway. Refs: #2277 --- buildchain/buildchain/iso.py | 2 +- scripts/{solution-manager.sh => solutions.sh} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{solution-manager.sh => solutions.sh} (100%) 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/scripts/solution-manager.sh b/scripts/solutions.sh similarity index 100% rename from scripts/solution-manager.sh rename to scripts/solutions.sh From 3167e92c411278b63afdbc8f6914c48c39b696a6 Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Mon, 2 Mar 2020 17:49:18 +0100 Subject: [PATCH 15/19] scripts: add helpers to common shell lib We add two helpers: - first one to retrieve the saltenv - second one to retrieve the local minion ID These functions will be used in the script managing the Solutions Refs: #2277 --- scripts/common.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) 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- +} From 28f98a64dbbe83d6b34b4e6244ccf6cc6e89caa9 Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Mon, 2 Mar 2020 17:51:19 +0100 Subject: [PATCH 16/19] scripts: rewrite solutions manager script The script has been rewritten to reflect changes done on how we manage Solutions. It also now manages Environment creation, deletion and edition. Refs: #2277 --- scripts/solutions.sh | 646 +++++++++++++++++++++++++++++-------------- 1 file changed, 435 insertions(+), 211 deletions(-) diff --git a/scripts/solutions.sh b/scripts/solutions.sh index f80767e537..5438852489 100755 --- a/scripts/solutions.sh +++ b/scripts/solutions.sh @@ -1,270 +1,494 @@ #!/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" +set -euo pipefail + +KUBECONFIG=${KUBECONFIG:-/etc/kubernetes/admin.conf} 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" +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 ]] } -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 +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}")" - -cat << EOF >> "${LOGFILE}" ---- MetalK8s solution manager started on $(date -u -R) --- -EOF +mkdir -p "$(dirname "$LOGFILE")" -exec > >(tee -ia "${LOGFILE}") 2>&1 +exec > >(tee -ia "$LOGFILE") 2>&1 cleanup() { - rm -rf "${TMPFILES}" || true + rm -rf "$TMPFILES" || true } trap cleanup EXIT +BASE_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +# shellcheck disable=SC1090 +. "$BASE_DIR"/common.sh -run_quiet() { - local name=$1 - shift 1 +check_command_mandatory_options() { + local -a missing_options=() + local -r command=$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) + for option in ${COMMAND_MANDATORY_OPTIONS[$command]:-}; do + [[ ${!OPTIONS_MAPPING[$option]:-} ]] || missing_options+=("$option") + done - local duration=$(( end - start )) + if (( ${#missing_options[@]} )); then + echo 1>&2 "Missing options for command '$command':" \ + "${missing_options[@]}" + return 1 + fi - if [ $RC -eq 0 ]; then - echo " done [${duration}s]" - else - echo " fail [${duration}s]" - cat >/dev/stderr << EOM + return 0 +} -Failure while running step '${name}' +salt_minion_exec() { + salt-call "$@" --retcode-passthrough +} -Command: $@ +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" +} -Output: +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)'}" +} -<< BEGIN >> -EOM - cat "${TMPFILES}/out" > /dev/stderr +deactivate_solution() { + run "Updating Solutions configuration file" \ + salt_minion_exec metalk8s_solutions.deactivate_solution \ + solution="$NAME" \ + --local - cat >/dev/stderr << EOM -<< END >> + run "Removing Solution components" \ + salt_master_exec salt-run state.orchestrate \ + metalk8s.orchestrate.solutions.deploy-components \ + pillar="{'bootstrap_id': '$(get_salt_minion_id)'}" +} -This script will now exit +configure_archives() { + local removed=${1:-False} -EOM + for archive in "${ARCHIVES[@]}"; do + salt_minion_exec metalk8s_solutions.configure_archive \ + archive="$archive" \ + removed="$removed" \ + create_config=True \ + --local || return + done - exit 1 - fi + salt_minion_exec saltutil.refresh_pillar } -run_verbose() { - local name=$1 - shift 1 +import_solution() { + SALTENV=${SALTENV:-$(get_salt_env)} - echo "> ${name}..." - "$@" + run "Updating Solutions configuration file" configure_archives + run "Importing Solutions" \ + salt_minion_exec state.sls metalk8s.solutions.available \ + saltenv="$SALTENV" } -run() { - if [ "$VERBOSE" -eq 1 ]; then - run_verbose "${@}" - else - run_quiet "${@}" - fi +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" } -die() { - echo 1>&2 "$@" - return 1 +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" } -# 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-)" +check_namespace() { + if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then + echo 1>&2 "Namespace '$NAMESPACE' does not exist" + return 1 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 + 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 } -_set_bootstrap_id() { - local -r bootstrap_id=$( - ${SALT_CALL} --local --out txt grains.get id \ - | awk '/^local\: /{ print $2 }' - ) - - PILLAR=( - "{" - " 'bootstrap_id': '$bootstrap_id'" - "}" - ) +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 -} -_init () { - _set_env - _check_salt_master - _set_bootstrap_id - if [ ! -f "$SOLUTION_CONFIG" ]; then - echo "archives: []" >"$SOLUTION_CONFIG" + 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" +} -# 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 +delete_namespaces() { + for namespace; do + run "Deleting Namespace '$namespace'" \ + kubectl delete namespace "$namespace" + done } -add_solutions() { - add=("$@") - for solution in "${add[@]}"; do - if ! containsElement "'$solution'" \ - "${EXISTENT_SOLUTIONS[@]+"${EXISTENT_SOLUTIONS[@]}"}"; then - EXISTENT_SOLUTIONS+=("'$solution'") +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 - done + else + check_namespace || return + namespaces=("$NAMESPACE") + fi + + delete_namespaces "${namespaces[@]}" } -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 +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" } -_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[@]}" +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 - 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 + + 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 - 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 $? + 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[*]}" } -_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[*]}" +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[*]}" } -# Main -_init -run "Add/Delete solution" _add_del_solution -run "Configure solutions" _configure_solutions +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]}" From 8fdf32d457e5dab582fff8ed7f9d4be7aebf1331 Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Tue, 3 Mar 2020 14:52:43 +0100 Subject: [PATCH 17/19] docs: update solutions script documentation Adapt the documentation with the options and command allowed by the rewriting of the solutions manager script Refs: #2277 --- docs/developer/solutions/deploy.rst | 133 +++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 10 deletions(-) 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 From 2b72ad2a54b47a29b149586492dbdb4b3bbaa8ea Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Thu, 5 Mar 2020 17:22:18 +0100 Subject: [PATCH 18/19] salt/script: Reconfigure registry after (un)mounting Solutions We no longer try to reconfigure registry by including metalk8s.repo.installed in metalk8s.solutions.available which is wrong: We alter the pillar by mounting Solutions and the changes are not taken into account inside the repo sls, because Jjinja rendering has already been done at this point, so it reconfigures registry with outdated configuration. We now do this in 2 steps, first mount/unmount Solutions, then reconfigure registry. Refs: #2277 --- salt/metalk8s/solutions/available.sls | 7 ------- scripts/solutions.sh | 6 ++++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/salt/metalk8s/solutions/available.sls b/salt/metalk8s/solutions/available.sls index b031666257..e67c9ff0b5 100644 --- a/salt/metalk8s/solutions/available.sls +++ b/salt/metalk8s/solutions/available.sls @@ -6,9 +6,6 @@ {%- from "metalk8s/map.jinja" import repo with context %} -include: - - metalk8s.repo.installed - {%- macro extract_info(archive_path) %} {{ machine_name }},{{ display_name }},{{ mount_path }} {%- endmacro %} @@ -74,8 +71,6 @@ Expose container images for Solution {{ display_name }}: registry_root: {{ mount_path }}/images - require: - file: Container images for Solution {{ display_name }} exist - - require_in: - - sls: metalk8s.repo.installed {%- endfor %} {# Configured solutions are all mounted and images exposed #} @@ -95,8 +90,6 @@ Cannot remove archive for active Solution {{ display_name }}: Remove container images for Solution {{ display_name }}: file.absent: - name: {{ repo.config.directory }}/{{ info.id }}-registry-config.inc - - require_in: - - sls: metalk8s.repo.installed Unmount Solution {{ display_name }}: mount.unmounted: diff --git a/scripts/solutions.sh b/scripts/solutions.sh index 5438852489..677f27f6ed 100755 --- a/scripts/solutions.sh +++ b/scripts/solutions.sh @@ -278,6 +278,9 @@ import_solution() { 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() { @@ -287,6 +290,9 @@ unimport_solution() { 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() { From ac115f9018495a1234efbc8fdabd603bbae0480d Mon Sep 17 00:00:00 2001 From: Alexandre Allard Date: Thu, 5 Mar 2020 17:28:07 +0100 Subject: [PATCH 19/19] salt: add a no-op if there is nothing to do in Solutions sls Adding of salt no-op to avoid having an empty sls file after rendering, otherwise it returns an exit code of 2 when using --retcode-passthrough, thus breaking the calling script. Refs: #2277 --- salt/metalk8s/solutions/available.sls | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/salt/metalk8s/solutions/available.sls b/salt/metalk8s/solutions/available.sls index e67c9ff0b5..bd02c7c4f9 100644 --- a/salt/metalk8s/solutions/available.sls +++ b/salt/metalk8s/solutions/available.sls @@ -10,14 +10,20 @@ {{ 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 #} - {%- set configured = pillar.metalk8s.solutions.config.archives %} {%- for archive_path in configured %} {%- set solution = salt['metalk8s.archive_info_from_iso'](archive_path) %} {%- set machine_name = solution.name | replace(' ', '-') | lower %} @@ -76,7 +82,6 @@ Expose container images for Solution {{ display_name }}: {#- Unmount all Solution ISOs mounted in /srv/scality not referenced in the configuration file #} - {%- set available = pillar.metalk8s.solutions.available %} {%- for machine_name, versions in available.items() %} {%- for info in versions %} {%- if info.archive not in configured %} @@ -94,7 +99,6 @@ Remove container images for Solution {{ display_name }}: Unmount Solution {{ display_name }}: mount.unmounted: - name: {{ info.mountpoint }} - - device: {{ info.archive }} - persist: True - require: - file: Remove container images for Solution {{ display_name }}