From 5e8107621eea7e556944db0688f909c93547f7ad Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Mon, 24 Jan 2022 15:44:43 -0500 Subject: [PATCH] Allow isolated paths as hostPath volume @ k8s/ocp/container groups --- awx/main/conf.py | 13 ++++++ awx/main/constants.py | 3 +- awx/main/tasks/jobs.py | 8 ++++ awx/main/tasks/receptor.py | 44 +++++++++++++++++++ awx/settings/defaults.py | 3 ++ .../screens/Setting/Jobs/JobsEdit/JobsEdit.js | 4 ++ .../JobsEdit/data.defaultJobSettings.json | 1 + 7 files changed, 75 insertions(+), 1 deletion(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index 6756347b54..c754ecc92a 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -334,6 +334,19 @@ register( category_slug='jobs', ) +register( + 'AWX_MOUNT_ISOLATED_PATHS_ON_K8S', + field_class=fields.BooleanField, + default=False, + label=_('Expose host paths for Container Groups'), + help_text=_( + 'Expose paths via hostPath for the Pods created by a Container Group. ' + 'HostPath volumes present many security risks, and it is a best practice to avoid the use of HostPaths when possible. ' + ), + category=_('Jobs'), + category_slug='jobs', +) + register( 'GALAXY_IGNORE_CERTS', field_class=fields.BooleanField, diff --git a/awx/main/constants.py b/awx/main/constants.py index 36209c3334..9074d9bd7f 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -88,7 +88,8 @@ JOB_FOLDER_PREFIX = 'awx_%s_' # :z option tells Podman that two containers share the volume content with r/w # :O option tells Podman to mount the directory from the host as a temporary storage using the overlay file system. +# :ro or :rw option to mount a volume in read-only or read-write mode, respectively. By default, the volumes are mounted read-write. # see podman-run manpage for further details # /HOST-DIR:/CONTAINER-DIR:OPTIONS -CONTAINER_VOLUMES_MOUNT_TYPES = ['z', 'O'] +CONTAINER_VOLUMES_MOUNT_TYPES = ['z', 'O', 'ro', 'rw'] MAX_ISOLATED_PATH_COLON_DELIMITER = 2 diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 7dea383014..54ce9b2f3d 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -40,6 +40,7 @@ from awx.main.constants import ( STANDARD_INVENTORY_UPDATE_ENV, JOB_FOLDER_PREFIX, MAX_ISOLATED_PATH_COLON_DELIMITER, + CONTAINER_VOLUMES_MOUNT_TYPES, ) from awx.main.models import ( Instance, @@ -164,6 +165,13 @@ class BaseTask(object): # Uppercase Z restricts access (in weird ways) to 1 container at a time if this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER: src, dest, scontext = this_path.split(':') + + # scontext validation via performed via API, but since this can be overriden via settings.py + # let's ensure scontext is one that we support + if scontext not in CONTAINER_VOLUMES_MOUNT_TYPES: + scontext = 'z' + logger.warn(f'The path {this_path} has volume mount type {scontext} which is not supported. Using "z" instead.') + params['container_volume_mounts'].append(f'{src}:{dest}:{scontext}') elif this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER - 1: src, dest = this_path.split(':') diff --git a/awx/main/tasks/receptor.py b/awx/main/tasks/receptor.py index 4cb0a543a2..2bdc0223ea 100644 --- a/awx/main/tasks/receptor.py +++ b/awx/main/tasks/receptor.py @@ -26,6 +26,8 @@ from awx.main.utils.common import ( parse_yaml_or_json, cleanup_new_process, ) +from awx.main.constants import MAX_ISOLATED_PATH_COLON_DELIMITER + # Receptorctl from receptorctl.socket_interface import ReceptorControl @@ -488,6 +490,48 @@ class AWXReceptorJob: if self.task.instance.execution_environment.pull: pod_spec['spec']['containers'][0]['imagePullPolicy'] = pull_options[self.task.instance.execution_environment.pull] + # This allows the user to also expose the isolated path list + # to EEs running in k8s/ocp environments, i.e. container groups. + # This assumes the node and SA supports hostPath volumes + # type is not passed due to backward compatibility, + # which means that no checks will be performed before mounting the hostPath volume. + if settings.AWX_MOUNT_ISOLATED_PATHS_ON_K8S and settings.AWX_ISOLATION_SHOW_PATHS: + spec_volume_mounts = [] + spec_volumes = [] + + for idx, this_path in enumerate(settings.AWX_ISOLATION_SHOW_PATHS): + scontext = None + if this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER: + src, dest, scontext = this_path.split(':') + elif this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER - 1: + src, dest = this_path.split(':') + else: + src = dest = this_path + + # Enforce read-only volume if 'ro' has been explicitly passed + # We do this so we can use the same configuration for regular scenarios and k8s + # Since flags like ':O', ':z' or ':Z' are not valid in the k8s realm + # Example: /data:/data:ro + read_only = bool('ro' == scontext) + + # Since type is not being passed, k8s by default will not perform any checks if the + # hostPath volume exists on the k8s node itself. + spec_volumes.append({'name': f'volume-{idx}', 'hostPath': {'path': src}}) + + spec_volume_mounts.append({'name': f'volume-{idx}', 'mountPath': f'{dest}', 'readOnly': read_only}) + + # merge any volumes definition already present in the pod_spec + if 'volumes' in pod_spec['spec']: + pod_spec['spec']['volumes'] += spec_volumes + else: + pod_spec['spec']['volumes'] = spec_volumes + + # merge any volumesMounts definition already present in the pod_spec + if 'volumeMounts' in pod_spec['spec']['containers'][0]: + pod_spec['spec']['containers'][0]['volumeMounts'] += spec_volume_mounts + else: + pod_spec['spec']['containers'][0]['volumeMounts'] = spec_volume_mounts + if self.task and self.task.instance.is_container_group_task: # If EE credential is passed, create an imagePullSecret if self.task.instance.execution_environment and self.task.instance.execution_environment.credential: diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9d0078916d..bc3c2549c3 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -1001,3 +1001,6 @@ DEFAULT_CONTROL_PLANE_QUEUE_NAME = 'controlplane' # For example, to disable SELinux in containers for podman # DEFAULT_CONTAINER_RUN_OPTIONS = ['--security-opt', 'label=disable'] DEFAULT_CONTAINER_RUN_OPTIONS = ['--network', 'slirp4netns:enable_ipv6=true'] + +# Mount exposed paths as hostPath resource in k8s/ocp +AWX_MOUNT_ISOLATED_PATHS_ON_K8S = False diff --git a/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js b/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js index fec8d6cdb8..22066243b7 100644 --- a/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js +++ b/awx/ui/src/screens/Setting/Jobs/JobsEdit/JobsEdit.js @@ -212,6 +212,10 @@ function JobsEdit() { name="AWX_ISOLATION_SHOW_PATHS" config={jobs.AWX_ISOLATION_SHOW_PATHS} /> + {submitError && } {revertError && } diff --git a/awx/ui/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json b/awx/ui/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json index 749249494a..9736f3794b 100644 --- a/awx/ui/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json +++ b/awx/ui/src/screens/Setting/Jobs/JobsEdit/data.defaultJobSettings.json @@ -27,6 +27,7 @@ "AWX_ISOLATION_SHOW_PATHS": [], "AWX_ROLES_ENABLED": true, "AWX_SHOW_PLAYBOOK_LINKS": false, + "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": false, "AWX_TASK_ENV": {}, "DEFAULT_INVENTORY_UPDATE_TIMEOUT": 0, "DEFAULT_JOB_TIMEOUT": 0,