From 08ffeb9c45099c4212ab9ca668fb8e064156b494 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Fri, 18 Sep 2026 08:28:24 +0000 Subject: [PATCH 01/13] Filter invalid mounts from apptainer_entrypoint and warn user --- src/deploy_tools/templates/apptainer_entrypoint | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index bb9d350e..b8201e80 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -17,6 +17,23 @@ command="{{ command }}" # Options and arguments to pass to command command_args="{{ command_args }}" +# Filter out any invalid mounts and warn the user +IFS="," read -ra entries <<< "$mounts" + +valid_mounts=() + +for entry in "${entries[@]}"; do + host_path="${entry%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + valid_mounts+=("$entry") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts=$(IFS=','; echo "${valid_mounts[*]}") + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 From 3ae2de2b0da92f98c7f55a4ca83fc74968022118 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Tue, 22 Sep 2026 12:12:04 +0000 Subject: [PATCH 02/13] Add optional mounts that do not result in errors when non-existant. When defining a list of mounts, you can now specify an 'optional_mount'. These are checked in the apptainer_entrypoint to see if the path exists. If not, then a warning is sent to stderr and the mount is excluded from the apptainer mount list. --- src/deploy_tools/app_builder.py | 10 ++++-- src/deploy_tools/models/apptainer_app.py | 33 ++++++++++++++++++- .../models/schemas/deployment.json | 26 ++++++++++++++- src/deploy_tools/models/schemas/module.json | 26 ++++++++++++++- src/deploy_tools/models/schemas/release.json | 26 ++++++++++++++- .../templates/apptainer_entrypoint | 7 ++-- 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/src/deploy_tools/app_builder.py b/src/deploy_tools/app_builder.py index 913ebc0b..61aafcd4 100644 --- a/src/deploy_tools/app_builder.py +++ b/src/deploy_tools/app_builder.py @@ -56,7 +56,12 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: options = entrypoint.options entrypoint_file = entrypoints_folder / entrypoint.name - mounts = ",".join(chain(global_options.mounts, options.mounts)).strip() + standard_mounts = ",".join( + chain(global_options.standard_mounts, options.standard_mounts) + ).strip() + optional_mounts = ",".join( + chain(global_options.optional_mounts, options.optional_mounts) + ).strip() host_binaries = " ".join( chain(global_options.host_binaries, options.host_binaries) ).strip() @@ -70,7 +75,8 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: command_args = command_args.strip() params = { - "mounts": mounts, + "standard_mounts": standard_mounts, + "optional_mounts": optional_mounts, "host_binaries": host_binaries, "apptainer_args": apptainer_args, "relative_sif_file": relative_sif_file, diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index 718e15b3..fcee7704 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -12,6 +12,23 @@ type MountPoint = Annotated[str, StringConstraints(pattern=MOUNT_REGEX)] +class OptionalMountPoint(ParentModel): + """Represents an optional mount point for the Apptainer container. + + When specified, the mount path is checked to see if it exists on the host. + If it does not exist, the mount is skipped and no error is raised. + """ + + optional_mount: Annotated[ + str, + StringConstraints(pattern=MOUNT_PATH_REGEX), + Field( + description="Path to mount in the container. " + "Will not error if the path does not exist" + ), + ] + + class EntrypointOptions(ParentModel): """Options applied when running an Apptainer entrypoint.""" @@ -25,7 +42,7 @@ class EntrypointOptions(ParentModel): ] = "" mounts: Annotated[ - list[MountPoint], + list[MountPoint | OptionalMountPoint], Field( description="A list of mount points to add to the container in the form of " "'host_path[:container_path[:opts]]' where opts (mount options) can be " @@ -42,6 +59,20 @@ class EntrypointOptions(ParentModel): ), ] = [] + @property + def standard_mounts(self) -> list[str]: + """Return a list of standard mount points from the mounts list.""" + return [mount for mount in self.mounts if isinstance(mount, str)] + + @property + def optional_mounts(self) -> list[str]: + """Return a list of optional mount points from the mounts list.""" + return [ + mount.optional_mount + for mount in self.mounts + if isinstance(mount, OptionalMountPoint) + ] + class Entrypoint(ParentModel): """Represents an entrypoint to a command on the Apptainer image. diff --git a/src/deploy_tools/models/schemas/deployment.json b/src/deploy_tools/models/schemas/deployment.json index 22fea3e2..77f1ffe7 100644 --- a/src/deploy_tools/models/schemas/deployment.json +++ b/src/deploy_tools/models/schemas/deployment.json @@ -188,7 +188,14 @@ "default": [], "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", "items": { - "$ref": "#/$defs/MountPoint" + "anyOf": [ + { + "$ref": "#/$defs/MountPoint" + }, + { + "$ref": "#/$defs/OptionalMountPoint" + } + ] }, "title": "Mounts", "type": "array" @@ -385,6 +392,23 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, + "OptionalMountPoint": { + "additionalProperties": false, + "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", + "properties": { + "optional_mount": { + "description": "Path to mount in the container. Will not error if the path does not exist", + "pattern": "/[^:]*", + "title": "Optional Mount", + "type": "string" + } + }, + "required": [ + "optional_mount" + ], + "title": "OptionalMountPoint", + "type": "object" + }, "Release": { "additionalProperties": false, "description": "Represents a Module along with its lifecycle (deprecation) status.", diff --git a/src/deploy_tools/models/schemas/module.json b/src/deploy_tools/models/schemas/module.json index ccac6706..88bfb15a 100644 --- a/src/deploy_tools/models/schemas/module.json +++ b/src/deploy_tools/models/schemas/module.json @@ -169,7 +169,14 @@ "default": [], "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", "items": { - "$ref": "#/$defs/MountPoint" + "anyOf": [ + { + "$ref": "#/$defs/MountPoint" + }, + { + "$ref": "#/$defs/OptionalMountPoint" + } + ] }, "title": "Mounts", "type": "array" @@ -254,6 +261,23 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, + "OptionalMountPoint": { + "additionalProperties": false, + "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", + "properties": { + "optional_mount": { + "description": "Path to mount in the container. Will not error if the path does not exist", + "pattern": "/[^:]*", + "title": "Optional Mount", + "type": "string" + } + }, + "required": [ + "optional_mount" + ], + "title": "OptionalMountPoint", + "type": "object" + }, "ShellApp": { "additionalProperties": false, "description": "Represents a Shell application.\n\nThis will run the code specified as a shell script. This currently uses Bash for\nimproved functionality while retaining high compatibility with various Linux\ndistributions.", diff --git a/src/deploy_tools/models/schemas/release.json b/src/deploy_tools/models/schemas/release.json index c625f215..6d1b4130 100644 --- a/src/deploy_tools/models/schemas/release.json +++ b/src/deploy_tools/models/schemas/release.json @@ -169,7 +169,14 @@ "default": [], "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", "items": { - "$ref": "#/$defs/MountPoint" + "anyOf": [ + { + "$ref": "#/$defs/MountPoint" + }, + { + "$ref": "#/$defs/OptionalMountPoint" + } + ] }, "title": "Mounts", "type": "array" @@ -366,6 +373,23 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, + "OptionalMountPoint": { + "additionalProperties": false, + "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", + "properties": { + "optional_mount": { + "description": "Path to mount in the container. Will not error if the path does not exist", + "pattern": "/[^:]*", + "title": "Optional Mount", + "type": "string" + } + }, + "required": [ + "optional_mount" + ], + "title": "OptionalMountPoint", + "type": "object" + }, "ShellApp": { "additionalProperties": false, "description": "Represents a Shell application.\n\nThis will run the code specified as a shell script. This currently uses Bash for\nimproved functionality while retaining high compatibility with various Linux\ndistributions.", diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index b8201e80..b48644c8 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -7,7 +7,8 @@ set -e # Mounts for container -mounts="{{ mounts }}" +standard_mounts="{{ standard_mounts }}" +optional_mounts="{{ optional_mounts }}" # Additional arguments for apptainer apptainer_args="{{ apptainer_args }}" # Sif file path @@ -18,7 +19,7 @@ command="{{ command }}" command_args="{{ command_args }}" # Filter out any invalid mounts and warn the user -IFS="," read -ra entries <<< "$mounts" +IFS="," read -ra entries <<< "$optional_mounts" valid_mounts=() @@ -32,7 +33,7 @@ for entry in "${entries[@]}"; do fi done -mounts=$(IFS=','; echo "${valid_mounts[*]}") +mounts="${standard_mounts},$(IFS=','; echo "${valid_mounts[*]}")" # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then From 2cd80fcf587169c2cd8e2b4e648ef65b84d887da Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Tue, 22 Sep 2026 12:15:44 +0000 Subject: [PATCH 03/13] Exclude optional mount code from apptainer_entrypoint if none defined --- src/deploy_tools/templates/apptainer_entrypoint | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index b48644c8..11def870 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -18,6 +18,7 @@ command="{{ command }}" # Options and arguments to pass to command command_args="{{ command_args }}" +{% if optional_mounts != None %} # Filter out any invalid mounts and warn the user IFS="," read -ra entries <<< "$optional_mounts" @@ -34,6 +35,7 @@ for entry in "${entries[@]}"; do done mounts="${standard_mounts},$(IFS=','; echo "${valid_mounts[*]}")" +{% endif %} # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then From 46cdf76c7c6d1892ed78cdd1320e1f326e8848c9 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Tue, 22 Sep 2026 12:43:12 +0000 Subject: [PATCH 04/13] Improve naming of variables for optional_mount code --- src/deploy_tools/templates/apptainer_entrypoint | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index 11def870..8b5a76c2 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -20,21 +20,21 @@ command_args="{{ command_args }}" {% if optional_mounts != None %} # Filter out any invalid mounts and warn the user -IFS="," read -ra entries <<< "$optional_mounts" +IFS="," read -ra mount_paths <<< "$optional_mounts" -valid_mounts=() +validated_mounts=() -for entry in "${entries[@]}"; do - host_path="${entry%%:*}" +for mount_path in "${mount_paths[@]}"; do + host_path="${mount_path%%:*}" if [[ -d "$host_path" || -f "$host_path" ]]; then - valid_mounts+=("$entry") + validated_mounts+=("$mount_path") else echo "WARNING: Unable to mount $host_path: path does not exist" >&2 fi done -mounts="${standard_mounts},$(IFS=','; echo "${valid_mounts[*]}")" +mounts="${standard_mounts},$(IFS=','; echo "${validated_mounts[*]}")" {% endif %} # Raise an error if sif file does not exist From fdd1c25bc7ee5c6d3838373f60e17993ccc79de8 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Wed, 23 Sep 2026 10:18:34 +0000 Subject: [PATCH 05/13] Fix the regex of optional mounts to match (normal) mounts --- src/deploy_tools/models/apptainer_app.py | 2 +- src/deploy_tools/models/schemas/deployment.json | 2 +- src/deploy_tools/models/schemas/module.json | 2 +- src/deploy_tools/models/schemas/release.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index fcee7704..17c589a9 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -21,7 +21,7 @@ class OptionalMountPoint(ParentModel): optional_mount: Annotated[ str, - StringConstraints(pattern=MOUNT_PATH_REGEX), + StringConstraints(pattern=MOUNT_REGEX), Field( description="Path to mount in the container. " "Will not error if the path does not exist" diff --git a/src/deploy_tools/models/schemas/deployment.json b/src/deploy_tools/models/schemas/deployment.json index 77f1ffe7..dd917871 100644 --- a/src/deploy_tools/models/schemas/deployment.json +++ b/src/deploy_tools/models/schemas/deployment.json @@ -398,7 +398,7 @@ "properties": { "optional_mount": { "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "/[^:]*", + "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "title": "Optional Mount", "type": "string" } diff --git a/src/deploy_tools/models/schemas/module.json b/src/deploy_tools/models/schemas/module.json index 88bfb15a..6f7c09d1 100644 --- a/src/deploy_tools/models/schemas/module.json +++ b/src/deploy_tools/models/schemas/module.json @@ -267,7 +267,7 @@ "properties": { "optional_mount": { "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "/[^:]*", + "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "title": "Optional Mount", "type": "string" } diff --git a/src/deploy_tools/models/schemas/release.json b/src/deploy_tools/models/schemas/release.json index 6d1b4130..be09639b 100644 --- a/src/deploy_tools/models/schemas/release.json +++ b/src/deploy_tools/models/schemas/release.json @@ -379,7 +379,7 @@ "properties": { "optional_mount": { "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "/[^:]*", + "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "title": "Optional Mount", "type": "string" } From ee85172c9da6755676ad7fe7a4fb535054e3425b Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Wed, 23 Sep 2026 10:21:05 +0000 Subject: [PATCH 06/13] update mount_paths in apptainer_entrypoint to optional_mount_paths --- src/deploy_tools/templates/apptainer_entrypoint | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index 8b5a76c2..ca9f6881 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -20,11 +20,11 @@ command_args="{{ command_args }}" {% if optional_mounts != None %} # Filter out any invalid mounts and warn the user -IFS="," read -ra mount_paths <<< "$optional_mounts" +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" validated_mounts=() -for mount_path in "${mount_paths[@]}"; do +for mount_path in "${optional_mount_paths[@]}"; do host_path="${mount_path%%:*}" if [[ -d "$host_path" || -f "$host_path" ]]; then From 850dd60a1d897deeb0118424d467f9e90f5a85fa Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Wed, 23 Sep 2026 10:27:40 +0000 Subject: [PATCH 07/13] Expand the description of mounts field to include optional_mounts --- src/deploy_tools/models/apptainer_app.py | 5 ++++- src/deploy_tools/models/schemas/deployment.json | 2 +- src/deploy_tools/models/schemas/module.json | 2 +- src/deploy_tools/models/schemas/release.json | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index 17c589a9..425940fb 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -46,7 +46,10 @@ class EntrypointOptions(ParentModel): Field( description="A list of mount points to add to the container in the form of " "'host_path[:container_path[:opts]]' where opts (mount options) can be " - "'ro' or 'rw' and defaults to 'rw'" + "'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a" + " string or an object with the key 'optional_mount' to indicate that the" + " mount is optional. Mounts defined as a string will error if the path does" + " not exist, whereas optional mounts will be excluded when non-existent." ), ] = [] diff --git a/src/deploy_tools/models/schemas/deployment.json b/src/deploy_tools/models/schemas/deployment.json index dd917871..aa50bb46 100644 --- a/src/deploy_tools/models/schemas/deployment.json +++ b/src/deploy_tools/models/schemas/deployment.json @@ -186,7 +186,7 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", "items": { "anyOf": [ { diff --git a/src/deploy_tools/models/schemas/module.json b/src/deploy_tools/models/schemas/module.json index 6f7c09d1..91545b5b 100644 --- a/src/deploy_tools/models/schemas/module.json +++ b/src/deploy_tools/models/schemas/module.json @@ -167,7 +167,7 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", "items": { "anyOf": [ { diff --git a/src/deploy_tools/models/schemas/release.json b/src/deploy_tools/models/schemas/release.json index be09639b..a109e113 100644 --- a/src/deploy_tools/models/schemas/release.json +++ b/src/deploy_tools/models/schemas/release.json @@ -167,7 +167,7 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'", + "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", "items": { "anyOf": [ { From 80e21d4587a0cd49d547c08bbd6bfbfebfea62c8 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Wed, 23 Sep 2026 13:55:14 +0000 Subject: [PATCH 08/13] Make optional mounts their own field under options --- src/deploy_tools/app_builder.py | 6 +-- src/deploy_tools/models/apptainer_app.py | 51 +++++-------------- .../models/schemas/deployment.json | 39 +++++--------- src/deploy_tools/models/schemas/module.json | 39 +++++--------- src/deploy_tools/models/schemas/release.json | 39 +++++--------- .../templates/apptainer_entrypoint | 4 +- 6 files changed, 57 insertions(+), 121 deletions(-) diff --git a/src/deploy_tools/app_builder.py b/src/deploy_tools/app_builder.py index 61aafcd4..0d62c5f9 100644 --- a/src/deploy_tools/app_builder.py +++ b/src/deploy_tools/app_builder.py @@ -56,9 +56,7 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: options = entrypoint.options entrypoint_file = entrypoints_folder / entrypoint.name - standard_mounts = ",".join( - chain(global_options.standard_mounts, options.standard_mounts) - ).strip() + mounts = ",".join(chain(global_options.mounts, options.mounts)).strip() optional_mounts = ",".join( chain(global_options.optional_mounts, options.optional_mounts) ).strip() @@ -75,7 +73,7 @@ def _create_apptainer_files(self, app: ApptainerApp, module: Module) -> None: command_args = command_args.strip() params = { - "standard_mounts": standard_mounts, + "mounts": mounts, "optional_mounts": optional_mounts, "host_binaries": host_binaries, "apptainer_args": apptainer_args, diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index 425940fb..d765fa5b 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -12,23 +12,6 @@ type MountPoint = Annotated[str, StringConstraints(pattern=MOUNT_REGEX)] -class OptionalMountPoint(ParentModel): - """Represents an optional mount point for the Apptainer container. - - When specified, the mount path is checked to see if it exists on the host. - If it does not exist, the mount is skipped and no error is raised. - """ - - optional_mount: Annotated[ - str, - StringConstraints(pattern=MOUNT_REGEX), - Field( - description="Path to mount in the container. " - "Will not error if the path does not exist" - ), - ] - - class EntrypointOptions(ParentModel): """Options applied when running an Apptainer entrypoint.""" @@ -42,14 +25,22 @@ class EntrypointOptions(ParentModel): ] = "" mounts: Annotated[ - list[MountPoint | OptionalMountPoint], + list[MountPoint], Field( - description="A list of mount points to add to the container in the form of " + description="A list of mount points that will result in an error if their " + "host paths cannot be found. This takes the form of " "'host_path[:container_path[:opts]]' where opts (mount options) can be " - "'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a" - " string or an object with the key 'optional_mount' to indicate that the" - " mount is optional. Mounts defined as a string will error if the path does" - " not exist, whereas optional mounts will be excluded when non-existent." + "'ro' or 'rw' and defaults to 'rw'. " + ), + ] = [] + + optional_mounts: Annotated[ + list[MountPoint], + Field( + description="A list of mount points that will not be mounted if their host " + "paths cannot be found (avoiding an error). This takes the form of " + "'host_path[:container_path[:opts]]' where opts (mount options) " + "can be 'ro' or 'rw' and defaults to 'rw'." ), ] = [] @@ -62,20 +53,6 @@ class EntrypointOptions(ParentModel): ), ] = [] - @property - def standard_mounts(self) -> list[str]: - """Return a list of standard mount points from the mounts list.""" - return [mount for mount in self.mounts if isinstance(mount, str)] - - @property - def optional_mounts(self) -> list[str]: - """Return a list of optional mount points from the mounts list.""" - return [ - mount.optional_mount - for mount in self.mounts - if isinstance(mount, OptionalMountPoint) - ] - class Entrypoint(ParentModel): """Represents an entrypoint to a command on the Apptainer image. diff --git a/src/deploy_tools/models/schemas/deployment.json b/src/deploy_tools/models/schemas/deployment.json index aa50bb46..35ffd0ba 100644 --- a/src/deploy_tools/models/schemas/deployment.json +++ b/src/deploy_tools/models/schemas/deployment.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -157,6 +158,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -186,20 +188,22 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", + "description": "A list of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { - "anyOf": [ - { - "$ref": "#/$defs/MountPoint" - }, - { - "$ref": "#/$defs/OptionalMountPoint" - } - ] + "$ref": "#/$defs/MountPoint" }, "title": "Mounts", "type": "array" }, + "optional_mounts": { + "default": [], + "description": "A list of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array" + }, "host_binaries": { "default": [], "description": "A list of host binaries to mount into the container. These are discovered on the host using the current PATH and are mounted into the container at /usr/bin/[binary_name]", @@ -392,23 +396,6 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, - "OptionalMountPoint": { - "additionalProperties": false, - "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", - "properties": { - "optional_mount": { - "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", - "title": "Optional Mount", - "type": "string" - } - }, - "required": [ - "optional_mount" - ], - "title": "OptionalMountPoint", - "type": "object" - }, "Release": { "additionalProperties": false, "description": "Represents a Module along with its lifecycle (deprecation) status.", diff --git a/src/deploy_tools/models/schemas/module.json b/src/deploy_tools/models/schemas/module.json index 91545b5b..cee67a00 100644 --- a/src/deploy_tools/models/schemas/module.json +++ b/src/deploy_tools/models/schemas/module.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -138,6 +139,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -167,20 +169,22 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", + "description": "A list of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { - "anyOf": [ - { - "$ref": "#/$defs/MountPoint" - }, - { - "$ref": "#/$defs/OptionalMountPoint" - } - ] + "$ref": "#/$defs/MountPoint" }, "title": "Mounts", "type": "array" }, + "optional_mounts": { + "default": [], + "description": "A list of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array" + }, "host_binaries": { "default": [], "description": "A list of host binaries to mount into the container. These are discovered on the host using the current PATH and are mounted into the container at /usr/bin/[binary_name]", @@ -261,23 +265,6 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, - "OptionalMountPoint": { - "additionalProperties": false, - "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", - "properties": { - "optional_mount": { - "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", - "title": "Optional Mount", - "type": "string" - } - }, - "required": [ - "optional_mount" - ], - "title": "OptionalMountPoint", - "type": "object" - }, "ShellApp": { "additionalProperties": false, "description": "Represents a Shell application.\n\nThis will run the code specified as a shell script. This currently uses Bash for\nimproved functionality while retaining high compatibility with various Linux\ndistributions.", diff --git a/src/deploy_tools/models/schemas/release.json b/src/deploy_tools/models/schemas/release.json index a109e113..b9a373d4 100644 --- a/src/deploy_tools/models/schemas/release.json +++ b/src/deploy_tools/models/schemas/release.json @@ -28,6 +28,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Global options that apply to all Entrypoints" @@ -138,6 +139,7 @@ "apptainer_args": "", "command_args": "", "mounts": [], + "optional_mounts": [], "host_binaries": [] }, "description": "Options to apply for this entrypoint" @@ -167,20 +169,22 @@ }, "mounts": { "default": [], - "description": "A list of mount points to add to the container in the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. Mounts can be specified as either a string or an object with the key 'optional_mount' to indicate that the mount is optional. Mounts defined as a string will error if the path does not exist, whereas optional mounts will be excluded when non-existent.", + "description": "A list of mount points that will result in an error if their host paths cannot be found. This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'. ", "items": { - "anyOf": [ - { - "$ref": "#/$defs/MountPoint" - }, - { - "$ref": "#/$defs/OptionalMountPoint" - } - ] + "$ref": "#/$defs/MountPoint" }, "title": "Mounts", "type": "array" }, + "optional_mounts": { + "default": [], + "description": "A list of mount points that will not be mounted if their host paths cannot be found (avoiding an error). This takes the form of 'host_path[:container_path[:opts]]' where opts (mount options) can be 'ro' or 'rw' and defaults to 'rw'.", + "items": { + "$ref": "#/$defs/MountPoint" + }, + "title": "Optional Mounts", + "type": "array" + }, "host_binaries": { "default": [], "description": "A list of host binaries to mount into the container. These are discovered on the host using the current PATH and are mounted into the container at /usr/bin/[binary_name]", @@ -373,23 +377,6 @@ "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", "type": "string" }, - "OptionalMountPoint": { - "additionalProperties": false, - "description": "Represents an optional mount point for the Apptainer container.\n\nWhen specified, the mount path is checked to see if it exists on the host.\nIf it does not exist, the mount is skipped and no error is raised.", - "properties": { - "optional_mount": { - "description": "Path to mount in the container. Will not error if the path does not exist", - "pattern": "^/[^:]*(:/[^:]*(:(ro|rw))?)?$", - "title": "Optional Mount", - "type": "string" - } - }, - "required": [ - "optional_mount" - ], - "title": "OptionalMountPoint", - "type": "object" - }, "ShellApp": { "additionalProperties": false, "description": "Represents a Shell application.\n\nThis will run the code specified as a shell script. This currently uses Bash for\nimproved functionality while retaining high compatibility with various Linux\ndistributions.", diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index ca9f6881..6ad13533 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -7,7 +7,7 @@ set -e # Mounts for container -standard_mounts="{{ standard_mounts }}" +mounts="{{ mounts }}" optional_mounts="{{ optional_mounts }}" # Additional arguments for apptainer apptainer_args="{{ apptainer_args }}" @@ -34,7 +34,7 @@ for mount_path in "${optional_mount_paths[@]}"; do fi done -mounts="${standard_mounts},$(IFS=','; echo "${validated_mounts[*]}")" +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" {% endif %} # Raise an error if sif file does not exist From b063a5cac8d01131cdc8c8de1e50331401f4b9b1 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Wed, 23 Sep 2026 13:56:13 +0000 Subject: [PATCH 09/13] Check there are no duplicate entries between mounts and optional mounts --- src/deploy_tools/models/apptainer_app.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index d765fa5b..705e55ae 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -1,6 +1,6 @@ from typing import Annotated, Literal -from pydantic import AnyUrl, Field, StringConstraints, UrlConstraints +from pydantic import AnyUrl, Field, StringConstraints, UrlConstraints, model_validator from .app import ENTRYPOINT_NAME_REGEX from .parent import ParentModel @@ -53,6 +53,17 @@ class EntrypointOptions(ParentModel): ), ] = [] + @model_validator(mode="after") + def check_unique_mounts(self) -> "EntrypointOptions": + """Ensure that mounts and optional_mounts do not contain duplicates.""" + duplicate_mounts = set(self.mounts).intersection(set(self.optional_mounts)) + if duplicate_mounts: + raise ValueError( + f"Duplicate paths found in mounts and optional_mounts: " + f"{duplicate_mounts}" + ) + return self + class Entrypoint(ParentModel): """Represents an entrypoint to a command on the Apptainer image. From 4122ab7c2236c1e402855e4c64471ac9fceca8f0 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Thu, 24 Sep 2026 13:22:40 +0000 Subject: [PATCH 10/13] Update golden master --- .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ .../deploy-tools-output/deployment.yaml | 2 ++ .../modules/apps/0.1/entrypoints/cowsay-hello | 18 ++++++++++++++++++ .../modules/apps/0.1/module.yaml | 2 ++ 18 files changed, 132 insertions(+) diff --git a/tests/samples/01-initial/deploy-tools-output/deployment.yaml b/tests/samples/01-initial/deploy-tools-output/deployment.yaml index 368ad142..46e25f73 100644 --- a/tests/samples/01-initial/deploy-tools-output/deployment.yaml +++ b/tests/samples/01-initial/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/01-initial/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/02-added/deploy-tools-output/deployment.yaml b/tests/samples/02-added/deploy-tools-output/deployment.yaml index 9270aa75..7d4b9130 100644 --- a/tests/samples/02-added/deploy-tools-output/deployment.yaml +++ b/tests/samples/02-added/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/02-added/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/03-updated/deploy-tools-output/deployment.yaml b/tests/samples/03-updated/deploy-tools-output/deployment.yaml index d2064d23..4786e656 100644 --- a/tests/samples/03-updated/deploy-tools-output/deployment.yaml +++ b/tests/samples/03-updated/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/03-updated/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml b/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml index df83de8b..8f0bcd67 100644 --- a/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml +++ b/tests/samples/04-deprecated/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/04-deprecated/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/05-restored/deploy-tools-output/deployment.yaml b/tests/samples/05-restored/deploy-tools-output/deployment.yaml index 869b8d29..598f926b 100644 --- a/tests/samples/05-restored/deploy-tools-output/deployment.yaml +++ b/tests/samples/05-restored/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/05-restored/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/06-removed/deploy-tools-output/deployment.yaml b/tests/samples/06-removed/deploy-tools-output/deployment.yaml index c2972e01..77ef38ad 100644 --- a/tests/samples/06-removed/deploy-tools-output/deployment.yaml +++ b/tests/samples/06-removed/deploy-tools-output/deployment.yaml @@ -17,11 +17,13 @@ releases: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: diff --git a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello index d43f0d44..41ba34a0 100755 --- a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello +++ b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/entrypoints/cowsay-hello @@ -8,6 +8,7 @@ set -e # Mounts for container mounts="" +optional_mounts="" # Additional arguments for apptainer apptainer_args="" # Sif file path @@ -17,6 +18,23 @@ command="cowsay" # Options and arguments to pass to command command_args="Hello" +# Filter out any invalid mounts and warn the user +IFS="," read -ra optional_mount_paths <<< "$optional_mounts" + +validated_mounts=() + +for mount_path in "${optional_mount_paths[@]}"; do + host_path="${mount_path%%:*}" + + if [[ -d "$host_path" || -f "$host_path" ]]; then + validated_mounts+=("$mount_path") + else + echo "WARNING: Unable to mount $host_path: path does not exist" >&2 + fi +done + +mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" + # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then echo "ERROR: sif file ${sif_file} does not exist" 1>&2 diff --git a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml index 60d6f708..86e1ca21 100644 --- a/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml +++ b/tests/samples/06-removed/deploy-tools-output/modules/apps/0.1/module.yaml @@ -12,11 +12,13 @@ applications: command_args: Hello host_binaries: [] mounts: [] + optional_mounts: [] global_options: apptainer_args: '' command_args: '' host_binaries: [] mounts: [] + optional_mounts: [] - app_type: shell name: test-echo-module-var script: From 5fbd1ec21f3d82cb1146c272d8aee66b013c6738 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Mon, 28 Sep 2026 09:01:57 +0000 Subject: [PATCH 11/13] Ensure entrypoint mounts are prioritised over global mounts --- src/deploy_tools/models/apptainer_app.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/deploy_tools/models/apptainer_app.py b/src/deploy_tools/models/apptainer_app.py index 705e55ae..b2595c77 100644 --- a/src/deploy_tools/models/apptainer_app.py +++ b/src/deploy_tools/models/apptainer_app.py @@ -143,3 +143,14 @@ class ApptainerApp(ParentModel): EntrypointOptions, Field(description="Global options that apply to all Entrypoints"), ] = EntrypointOptions() + + @model_validator(mode="after") + def prioritise_entrypoint_mounts(self) -> "ApptainerApp": + """Allow entrypoint options to override mounts defined in global_options""" + for entrypoint in self.entrypoints: + for mount in entrypoint.options.mounts + entrypoint.options.optional_mounts: + if mount in self.global_options.mounts: + self.global_options.mounts.remove(mount) + if mount in self.global_options.optional_mounts: + self.global_options.optional_mounts.remove(mount) + return self From 3c98cc1ff4397f88c7424500e955d0c2eaceb884 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Tue, 29 Sep 2026 15:13:26 +0000 Subject: [PATCH 12/13] exclude optional_mount code from entrypoint if length=0 --- src/deploy_tools/templates/apptainer_entrypoint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index 6ad13533..1a304b89 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -18,7 +18,7 @@ command="{{ command }}" # Options and arguments to pass to command command_args="{{ command_args }}" -{% if optional_mounts != None %} +{% if optional_mounts|length %} # Filter out any invalid mounts and warn the user IFS="," read -ra optional_mount_paths <<< "$optional_mounts" From 5582a4c37f10f9a05e439b1ef62b8efa70170316 Mon Sep 17 00:00:00 2001 From: MichaelStubbings Date: Tue, 29 Sep 2026 15:14:14 +0000 Subject: [PATCH 13/13] Add whitespace handling to entrypoint template --- src/deploy_tools/templates/apptainer_entrypoint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deploy_tools/templates/apptainer_entrypoint b/src/deploy_tools/templates/apptainer_entrypoint index 1a304b89..d256df11 100644 --- a/src/deploy_tools/templates/apptainer_entrypoint +++ b/src/deploy_tools/templates/apptainer_entrypoint @@ -35,7 +35,7 @@ for mount_path in "${optional_mount_paths[@]}"; do done mounts="${mounts},$(IFS=','; echo "${validated_mounts[*]}")" -{% endif %} +{% endif -%} # Raise an error if sif file does not exist if [[ ! -f ${sif_file} ]]; then