diff --git a/charts/dex.yaml b/charts/dex.yaml index 7ff7ba13ef..6214b4b702 100644 --- a/charts/dex.yaml +++ b/charts/dex.yaml @@ -40,6 +40,10 @@ extraVolumeMounts: - name: dex-login mountPath: /web/themes/scality +podAnnotations: + # Override default checksum as we want to manage it with salt + checksum/config: '__slot__:salt:metalk8s_kubernetes.get_object_digest(kind="Secret", apiVersion="v1", namespace="metalk8s-auth", name="dex", object_key="data:config.yaml")' + certs: web: create: false diff --git a/docs/operation/cluster_and_service_configuration.rst b/docs/operation/cluster_and_service_configuration.rst index 5e8cef7e7c..663d946264 100644 --- a/docs/operation/cluster_and_service_configuration.rst +++ b/docs/operation/cluster_and_service_configuration.rst @@ -143,13 +143,6 @@ To add a new static user, perform the following operations: salt-master-bootstrap -- salt-run \\ state.sls metalk8s.addons.dex.deployed saltenv=metalk8s-|version| -#. From the Bootstrap node, restart the Dex deployments. - - .. code-block:: shell - - root@bootstrap $ kubectl --kubeconfig /etc/kubernetes/admin.conf \ - rollout restart deployment dex -n metalk8s-auth - #. Finally, create and apply the required :file:`ClusterRoleBinding.yaml` file that ensures that the newly added static user is bound to a Cluster Role. @@ -266,13 +259,6 @@ To change the password of an existing user, perform the following operations: salt-master-bootstrap -- salt-run \\ state.sls metalk8s.addons.dex.deployed saltenv=metalk8s-|version| -#. From the Bootstrap node, restart the Dex deployments. - - .. code-block:: shell - - root@bootstrap $ kubectl --kubeconfig /etc/kubernetes/admin.conf \ - rollout restart deployment dex -n metalk8s-auth - #. Verify that the password has been changed and you can log in to the MetalK8s UI using the new password diff --git a/salt/_modules/metalk8s.py b/salt/_modules/metalk8s.py index 64cd19cff2..d9d8afc032 100644 --- a/salt/_modules/metalk8s.py +++ b/salt/_modules/metalk8s.py @@ -10,6 +10,7 @@ from salt.pillar import get_pillar from salt.exceptions import CommandExecutionError +import salt.utils.args import salt.utils.files log = logging.getLogger(__name__) @@ -287,3 +288,40 @@ def check_pillar_keys(keys, refresh=True, pillar=None, raise_error=True): return False return True + + +def format_slots(data): + """Helper to replace slots in nested dictionnary + + "__slots__:salt:module.function(arg1, arg2, kwarg1=abc, kwargs2=cde) + + Arguments: + data: Data structure to format + """ + slots_callers = { + "salt": __salt__ + } + + if isinstance(data, list): + return [format_slots(elt) for elt in data] + + if isinstance(data, dict): + return {key: format_slots(value) for key, value in data.items()} + + if isinstance(data, basestring) and data.startswith('__slot__:'): + fmt = data.split(":", 2) + if len(fmt) != 3: + log.warning("Malformed slot: %s", data) + return data + if fmt[1] not in slots_callers: + log.warning( + "Malformed slot, only %s supported: %s", + slots_callers.keys(), data + ) + return data + + fun, args, kwargs = salt.utils.args.parse_function(fmt[2]) + + return slots_callers[fmt[1]][fun](*args, **kwargs) + + return data diff --git a/salt/_modules/metalk8s_kubernetes.py b/salt/_modules/metalk8s_kubernetes.py index 1da1582f94..b6bb53bdbf 100644 --- a/salt/_modules/metalk8s_kubernetes.py +++ b/salt/_modules/metalk8s_kubernetes.py @@ -133,6 +133,9 @@ def method(manifest=None, name=None, kind=None, apiVersion=None, ) ) + # Format slots on the manifest + manifest = __salt__.metalk8s.format_slots(manifest) + # Adding label containing metalk8s version (retrieved from saltenv) if action in ['create', 'replace']: match = re.search(r'^metalk8s-(?P.+)$', saltenv) @@ -351,3 +354,37 @@ def list_objects(kind, apiVersion, namespace='default', all_namespaces=False, raise CommandExecutionError('{}: {!s}'.format(base_msg, exc)) return [obj.to_dict() for obj in result.items] + + +def get_object_digest(object_key=None, checksum='sha256', *args, **kwargs): + """ + Helper to get the digest of one kubernetes object or from a specific key + of this object (usefull to get the digest of one config from ConfigMap) + + CLI Examples: + + .. code-block:: bash + + salt-call metalk8s_kubernetes.get_object_digest kind="ConfigMap" apiVersion="v1" name="my-config-map" object_key="config.yaml" + salt-call metalk8s_kubernetes.get_object_digest kind="Pod" apiVersion="v1" name="my-pod" + """ + obj = get_object(*args, **kwargs) + + if not obj: + raise CommandExecutionError('Unable to find the object') + + if object_key: + if not isinstance(object_key, list): + object_key = object_key.split(':') + + for key in object_key: + try: + obj = obj[key] + except KeyError: + raise CommandExecutionError( + 'Unable to find key "{}" in the object'.format( + '.'.join(object_key) + ) + ) + + return __salt__.hashutil.digest(str(obj), checksum=checksum) diff --git a/salt/metalk8s/addons/dex/deployed/chart.sls b/salt/metalk8s/addons/dex/deployed/chart.sls index 0a55910064..5f63235daa 100644 --- a/salt/metalk8s/addons/dex/deployed/chart.sls +++ b/salt/metalk8s/addons/dex/deployed/chart.sls @@ -187,7 +187,8 @@ spec: template: metadata: annotations: - checksum/config: d58a2489f8f7fd4df3f78cad5ea6ac51e7eda9ca076c41689ce853539ff2a15b + checksum/config: __slot__:salt:metalk8s_kubernetes.get_object_digest(kind="Secret", + apiVersion="v1", namespace="metalk8s-auth", name="dex", object_key="data:config.yaml") labels: app.kubernetes.io/component: dex app.kubernetes.io/instance: dex diff --git a/tests/post/features/service_configuration.feature b/tests/post/features/service_configuration.feature index fca29d74fb..2a2af8d1c8 100644 --- a/tests/post/features/service_configuration.feature +++ b/tests/post/features/service_configuration.feature @@ -8,6 +8,17 @@ Feature: Cluster and Services Configurations When we update the CSC 'spec.deployment.replicas' to '3' Then we have '3' at 'status.available_replicas' for 'dex' Deployment in namespace 'metalk8s-auth' + Scenario: Update Admin static user password + Given: the Kubernetes API is available + And we have a 'metalk8s-dex-config' CSC in namespace 'metalk8s-auth' + # NOTE: Consider that admin user is the first of the userlist + # Update password to "new-password" + When we update the CSC 'spec.localuserstore.userlist.0.hash' to '$2y$14$pDe0vj917rR3XJQ5iEZvhuSGoWkZg2/qBN/mMLqwFSz9S7EYcbIpO' + Then the control-plane Ingress path '/oidc' is available + And pods with label 'app.kubernetes.io/name=dex' are 'Ready' + And we are not able to login to Dex as 'admin@metalk8s.invalid' using password 'password' + And we are able to login to Dex as 'admin@metalk8s.invalid' using password 'new-password' + Scenario: Customization of pre-defined Prometheus rules Given the Kubernetes API is available And pods with label 'app=prometheus' are 'Ready' diff --git a/tests/post/steps/test_authentication.py b/tests/post/steps/test_authentication.py index 994d1cffc2..0aeca9d65f 100644 --- a/tests/post/steps/test_authentication.py +++ b/tests/post/steps/test_authentication.py @@ -218,6 +218,42 @@ def successful_login(host, context, status_code): assert auth_response.headers.get('location') is not None +@then(parsers.parse( + "we are able to login to Dex as '{username}' using password '{password}'")) +def dex_login_success( + host, + control_plane_ip, + username, + password, + context, + request_retry_session +): + dex_login( + host, control_plane_ip, + username, password, + context, request_retry_session + ) + successful_login(host, context, '303') + + +@then(parsers.parse( + "we are not able to login to Dex as '{username}' using password " + "'{password}'")) +def dex_login_fail( + host, + control_plane_ip, + username, + password, + context, + request_retry_session +): + dex_login( + host, control_plane_ip, + username, password, + context, request_retry_session + ) + failed_login(host, context) + # }}} diff --git a/tests/post/steps/test_service_configuration.py b/tests/post/steps/test_service_configuration.py index 1ab35edef0..23a8415783 100644 --- a/tests/post/steps/test_service_configuration.py +++ b/tests/post/steps/test_service_configuration.py @@ -18,6 +18,12 @@ def test_service_config_propagation(host): pass +@scenario('../features/service_configuration.feature', + 'Update Admin static user password') +def test_static_user_change(host): + pass + + @scenario('../features/service_configuration.feature', 'Customization of pre-defined Prometheus rules') def test_prometheus_rules_customization(host): diff --git a/tests/utils.py b/tests/utils.py index ff27b23450..03609ce130 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -167,7 +167,11 @@ def get_dict_element(data, path, delimiter='.'): Traverse a dict using a 'delimiter' on a target string. getitem(a, b) returns the value of a at index b """ - return functools.reduce(operator.getitem, path.split(delimiter), data) + return functools.reduce( + operator.getitem, + (int(k) if k.isdigit() else k for k in path.split(delimiter)), + data + ) def set_dict_element(data, path, value, delimiter='.'): @@ -176,9 +180,12 @@ def set_dict_element(data, path, value, delimiter='.'): and replace the value of a key """ current = data - elements = path.split(delimiter) + elements = [int(k) if k.isdigit() else k for k in path.split(delimiter)] for element in elements[:-1]: - current = current.setdefault(element, {}) + if isinstance(element, int): + current = current[element] if len(current) > element else [] + else: + current = current.setdefault(element, {}) current[elements[-1]] = value