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..23bf8faa68 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,
@@ -163,8 +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(':')
- params['container_volume_mounts'].append(f'{src}:{dest}:{scontext}')
+ src, dest, mount_option = this_path.split(':')
+
+ # 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}:{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 0321a50c80..d2809e27ac 100644
--- a/awx/main/tasks/receptor.py
+++ b/awx/main/tasks/receptor.py
@@ -24,6 +24,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
@@ -478,6 +480,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):
+ mount_option = None
+ if this_path.count(':') == MAX_ISOLATED_PATH_COLON_DELIMITER:
+ src, dest, mount_option = 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' == 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.
+ 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/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/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,
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
}