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 1/3] 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, From 169da866f3561434bbd3b21fad0dac3c82365cff Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 1 Feb 2022 15:47:10 -0500 Subject: [PATCH 2/3] Add UI unit tests to job settings --- .../Setting/Jobs/JobsDetail/JobsDetail.test.js | 1 + .../Setting/shared/data.allSettingOptions.json | 17 +++++++++++++++++ .../Setting/shared/data.allSettings.json | 3 ++- .../Setting/shared/data.jobSettings.json | 3 ++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/awx/ui/src/screens/Setting/Jobs/JobsDetail/JobsDetail.test.js b/awx/ui/src/screens/Setting/Jobs/JobsDetail/JobsDetail.test.js index 9b0dda4233..97110e3169 100644 --- a/awx/ui/src/screens/Setting/Jobs/JobsDetail/JobsDetail.test.js +++ b/awx/ui/src/screens/Setting/Jobs/JobsDetail/JobsDetail.test.js @@ -69,6 +69,7 @@ describe('', () => { assertDetail(wrapper, 'Default Project Update Timeout', '0 seconds'); assertDetail(wrapper, 'Per-Host Ansible Fact Cache Timeout', '0 seconds'); assertDetail(wrapper, 'Maximum number of forks per job', '200'); + assertDetail(wrapper, 'Expose host paths for Container Groups', 'Off'); assertVariableDetail( wrapper, 'Ansible Modules Allowed for Ad Hoc Jobs', diff --git a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json index 21cdae90c6..ab0bc3f8e1 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettingOptions.json @@ -276,6 +276,15 @@ "category_slug": "jobs", "default": false }, + "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": { + "type": "boolean", + "required": 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", + "default": false + }, "GALAXY_IGNORE_CERTS": { "type": "boolean", "required": false, @@ -3973,6 +3982,14 @@ "category_slug": "jobs", "defined_in_file": false }, + "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": { + "type": "boolean", + "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", + "defined_in_file": false + }, "GALAXY_IGNORE_CERTS": { "type": "boolean", "label": "Ignore Ansible Galaxy SSL Certificate Verification", diff --git a/awx/ui/src/screens/Setting/shared/data.allSettings.json b/awx/ui/src/screens/Setting/shared/data.allSettings.json index 4715c4e03e..555713c239 100644 --- a/awx/ui/src/screens/Setting/shared/data.allSettings.json +++ b/awx/ui/src/screens/Setting/shared/data.allSettings.json @@ -297,5 +297,6 @@ "users":{"fields":["username"],"adj_list":[]}, "instances":{"fields":["hostname"],"adj_list":[]} }, - "DEFAULT_EXECUTION_ENVIRONMENT": 1 + "DEFAULT_EXECUTION_ENVIRONMENT": 1, + "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": false } diff --git a/awx/ui/src/screens/Setting/shared/data.jobSettings.json b/awx/ui/src/screens/Setting/shared/data.jobSettings.json index 1815cc12b7..e24eedb36d 100644 --- a/awx/ui/src/screens/Setting/shared/data.jobSettings.json +++ b/awx/ui/src/screens/Setting/shared/data.jobSettings.json @@ -21,5 +21,6 @@ "DEFAULT_INVENTORY_UPDATE_TIMEOUT": 0, "DEFAULT_PROJECT_UPDATE_TIMEOUT": 0, "ANSIBLE_FACT_CACHE_TIMEOUT": 0, - "MAX_FORKS": 200 + "MAX_FORKS": 200, + "AWX_MOUNT_ISOLATED_PATHS_ON_K8S": false } From 864514729287f502dedf9153cbf946c53ba742c0 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Fri, 4 Feb 2022 12:45:02 -0500 Subject: [PATCH 3/3] Renamed scontext variable to mount_options --- awx/main/tasks/jobs.py | 13 ++++++------- awx/main/tasks/receptor.py | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 54ce9b2f3d..23bf8faa68 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -164,15 +164,14 @@ class BaseTask(object): # Using z allows the dir to be mounted by multiple containers # 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(':') + src, dest, mount_option = 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.') + # mount_option validation via performed via API, but since this can be overriden via settings.py + if mount_option not in CONTAINER_VOLUMES_MOUNT_TYPES: + mount_option = 'z' + logger.warn(f'The path {this_path} has volume mount type {mount_option} which is not supported. Using "z" instead.') - params['container_volume_mounts'].append(f'{src}:{dest}:{scontext}') + params['container_volume_mounts'].append(f'{src}:{dest}:{mount_option}') elif this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER - 1: src, dest = this_path.split(':') params['container_volume_mounts'].append(f'{src}:{dest}:z') diff --git a/awx/main/tasks/receptor.py b/awx/main/tasks/receptor.py index 2bdc0223ea..278f4dbbc6 100644 --- a/awx/main/tasks/receptor.py +++ b/awx/main/tasks/receptor.py @@ -500,9 +500,9 @@ class AWXReceptorJob: spec_volumes = [] for idx, this_path in enumerate(settings.AWX_ISOLATION_SHOW_PATHS): - scontext = None + mount_option = None if this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER: - src, dest, scontext = this_path.split(':') + src, dest, mount_option = this_path.split(':') elif this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER - 1: src, dest = this_path.split(':') else: @@ -512,7 +512,7 @@ class AWXReceptorJob: # 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) + read_only = bool('ro' == mount_option) # Since type is not being passed, k8s by default will not perform any checks if the # hostPath volume exists on the k8s node itself.