mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 02:12:12 +00:00
Add owner-aware OCI tmpfs mounts
This commit is contained in:
+2
-1
@@ -504,8 +504,9 @@ A profile may also change the image, with `image`, for projects that publish one
|
||||
| `external_services` | Names of services the application needs already running, such as `["MariaDB"]`. They are announced before the installation asks how to reach them |
|
||||
| `generated_files` | Files written in the container before its first start, each with `container_path`, `content`, `owner` and `mode`. With `"only_if_missing": true` a file that already exists in a kept volume is not replaced |
|
||||
| `volume_owner` | `uid` and `gid` of the user the application writes with, when it is not 1000 |
|
||||
| `tmpfs_mounts` | Volatile paths with a size and mount options. An optional `owner_strategy` sets ownership to `mapped-root`, `mapped-application-user` (using `uid`/`gid` or `PUID`/`PGID`-style variables), or `image-user` (the effective `lxc.init.uid`/`lxc.init.gid`); `mode` sets permissions. The exact `/run` path requires an owner strategy. |
|
||||
| `non_persistent_image_volumes` | Paths the image declares as volumes that are left inside the rootfs |
|
||||
| `runtime`, `resources`, `security`, `device_requests`, `tmpfs_mounts`, `extra_hosts` | What the Compose keys of the [translation table](#from-a-compose-file-or-a-docker-run-command-to-an-oci-compose) become |
|
||||
| `runtime`, `resources`, `security`, `device_requests`, `extra_hosts` | What the Compose keys of the [translation table](#from-a-compose-file-or-a-docker-run-command-to-an-oci-compose) become |
|
||||
|
||||
### `proxmox.security_profile`: privileges
|
||||
|
||||
|
||||
@@ -400,11 +400,27 @@
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": true
|
||||
}
|
||||
},
|
||||
"tmpfs_mounts": [
|
||||
{
|
||||
"id": "runtime",
|
||||
"container_path": "/run",
|
||||
"default_size_mb": 64,
|
||||
"minimum_size_mb": 16,
|
||||
"prompt_size": false,
|
||||
"mount_options": [
|
||||
"rw",
|
||||
"nosuid",
|
||||
"nodev"
|
||||
],
|
||||
"owner_strategy": "image-user",
|
||||
"mode": "0755"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": false,
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"image",
|
||||
@@ -414,7 +430,7 @@
|
||||
"environment"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Excluded from the installable catalog pending compatibility review. The tested image runs as nobody, but its nginx and PHP-FPM startup requires writable runtime paths under /run; the current native OCI-LXC recipe could not start the service. Do not install until a supported runtime mapping is confirmed."
|
||||
"policy": "The image runs nginx and PHP-FPM as its OCI image user. ProxMenux provides a native /run tmpfs owned by that effective LXC init user, with writable mode for the runtime files. The recipe remains unvalidated until a clean install passes its startup health check."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
|
||||
@@ -400,11 +400,27 @@
|
||||
"request_timeout_seconds": 10,
|
||||
"stability_seconds": 4,
|
||||
"verify_tls": true
|
||||
}
|
||||
},
|
||||
"tmpfs_mounts": [
|
||||
{
|
||||
"id": "runtime",
|
||||
"container_path": "/run",
|
||||
"default_size_mb": 64,
|
||||
"minimum_size_mb": 16,
|
||||
"prompt_size": false,
|
||||
"mount_options": [
|
||||
"rw",
|
||||
"nosuid",
|
||||
"nodev"
|
||||
],
|
||||
"owner_strategy": "image-user",
|
||||
"mode": "0755"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": false,
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"image",
|
||||
@@ -414,7 +430,7 @@
|
||||
"environment"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"policy": "Excluded from the installable catalog pending compatibility review. The tested image runs as nobody, but its nginx and PHP-FPM startup requires writable runtime paths under /run; the current native OCI-LXC recipe could not start the service. Do not install until a supported runtime mapping is confirmed."
|
||||
"policy": "The image runs nginx and PHP-FPM as its OCI image user. ProxMenux provides a native /run tmpfs owned by that effective LXC init user, with writable mode for the runtime files. The recipe remains unvalidated until a clean install passes its startup health check."
|
||||
},
|
||||
"validation": {
|
||||
"schema": "passed-at-generation",
|
||||
|
||||
@@ -6194,7 +6194,7 @@
|
||||
"curated_path": "catalog/curated/input.json",
|
||||
"template": "apps/input.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"automatic_install_candidate": false,
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
@@ -6202,7 +6202,7 @@
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"hidden": true,
|
||||
"hidden": false,
|
||||
"template_id": "image-input"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2005,17 +2005,6 @@ for MOUNT_WARNING in ${MOUNT_WARNINGS[@]+"${MOUNT_WARNINGS[@]}"}; do
|
||||
msg_warn "$MOUNT_WARNING"
|
||||
done
|
||||
|
||||
while IFS=$'\t' read -r TARGET SIZE_MB OPTIONS; do
|
||||
[[ -n $TARGET ]] || continue
|
||||
[[ $TARGET == /* && $TARGET != *","* && $TARGET != *[[:space:]]* ]] \
|
||||
|| die "$(translate "Invalid tmpfs path:") $TARGET"
|
||||
[[ $SIZE_MB =~ ^[0-9]+$ && $SIZE_MB -gt 0 ]] || die "$(translate "Invalid tmpfs size:") $TARGET"
|
||||
[[ $OPTIONS =~ ^(rw|ro|nosuid|nodev|noexec|mode=0[0-7]{3})(,(rw|ro|nosuid|nodev|noexec|mode=0[0-7]{3}))*$ ]] || die "$(translate "Invalid tmpfs options:") $TARGET"
|
||||
TARGET_RELATIVE=${TARGET#/}
|
||||
printf 'lxc.mount.entry: tmpfs %s tmpfs %s,size=%sM,create=dir 0 0\n' \
|
||||
"$TARGET_RELATIVE" "$OPTIONS" "$SIZE_MB" >>"$CONF"
|
||||
done < <(jq -r '.tmpfs_mounts[]? | [.container_path,.size_mb,(.mount_options | join(","))] | @tsv' "$DEPLOYMENT_FILE")
|
||||
|
||||
DEVICE_INDEX=0
|
||||
RESOLVED_CHARACTER_DEVICES=()
|
||||
NVIDIA_RUNTIME_CONFIGURED=0
|
||||
@@ -2101,6 +2090,13 @@ prepare_managed_format_directories() {
|
||||
prepare_managed_format_directories "/var/lib/lxc/${VMID}/rootfs"
|
||||
apply_extra_hosts
|
||||
apply_installer_profile
|
||||
# Render tmpfs entries after the image and installer profile have set the
|
||||
# effective LXC init uid/gid. The same renderer validates and records them.
|
||||
TMPFS_CONFIG=$(mktemp)
|
||||
python3 "${SCRIPT_DIR}/oci_runtime_settings.py" --render-tmpfs "$DEPLOYMENT_FILE" "$CONF" \
|
||||
>"$TMPFS_CONFIG" || { rm -f "$TMPFS_CONFIG"; die "$(translate "Could not configure the tmpfs mounts")"; }
|
||||
cat "$TMPFS_CONFIG" >>"$CONF"
|
||||
rm -f "$TMPFS_CONFIG"
|
||||
ensure_runtime_home
|
||||
apply_rlimits
|
||||
apply_host_monitor
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Validate declared volatile mounts and native network sysctl includes."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -25,15 +27,63 @@ def sysctl_content(deployment):
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def tmpfs_lines(deployment):
|
||||
def _config_ids(config):
|
||||
"""Read the effective OCI init identity written by Proxmox."""
|
||||
if config is None:
|
||||
return 0, 0
|
||||
text = config.decode() if isinstance(config, bytes) else str(config)
|
||||
values = {}
|
||||
for line in text.splitlines():
|
||||
if ': ' not in line:
|
||||
continue
|
||||
key, value = line.split(': ', 1)
|
||||
if key in ('lxc.init.uid', 'lxc.init.gid'):
|
||||
if key in values:
|
||||
raise ValueError(translate('The OCI image user or group is ambiguous'))
|
||||
if not re.fullmatch(r'[0-9]{1,10}', value):
|
||||
raise ValueError(translate('The OCI image user or group is not numeric'))
|
||||
number = int(value)
|
||||
if number > 4294967294:
|
||||
raise ValueError(translate('The OCI image user or group is outside the supported range'))
|
||||
values[key] = number
|
||||
return values.get('lxc.init.uid', 0), values.get('lxc.init.gid', 0)
|
||||
|
||||
|
||||
def _application_ids(deployment, item):
|
||||
"""Resolve Compose-style application IDs inside the LXC user namespace."""
|
||||
environment = {}
|
||||
for variable in deployment.get('environment', []):
|
||||
environment[variable.get('name')] = variable.get('value')
|
||||
uid = item.get('uid')
|
||||
gid = item.get('gid')
|
||||
if uid is None:
|
||||
uid = next((environment[name] for name in ('PUID', 'USER_ID', 'UID')
|
||||
if environment.get(name) not in (None, '')), 0)
|
||||
if gid is None:
|
||||
gid = next((environment[name] for name in ('PGID', 'GROUP_ID', 'GID')
|
||||
if environment.get(name) not in (None, '')), 0)
|
||||
result = []
|
||||
for value in (uid, gid):
|
||||
if isinstance(value, bool) or not re.fullmatch(r'[0-9]{1,10}', str(value)):
|
||||
raise ValueError(translate('The application user or group is not numeric'))
|
||||
number = int(value)
|
||||
if number > 4294967294:
|
||||
raise ValueError(translate('The application user or group is outside the supported range'))
|
||||
result.append(number)
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def tmpfs_lines(deployment, config=None):
|
||||
result = []
|
||||
targets = set()
|
||||
persistent = [m['container_path'].rstrip('/') for m in deployment.get('mounts', [])]
|
||||
for item in deployment.get('tmpfs_mounts', []):
|
||||
target, size = item['container_path'], item['size_mb']
|
||||
owner_strategy = item.get('owner_strategy')
|
||||
if (not re.fullmatch(r'/[A-Za-z0-9_./-]+', target) or '..' in target.split('/')
|
||||
or '//' in target or target.endswith('/') or target in ('/etc','/usr','/bin','/lib','/lib64','/sbin','/proc','/sys','/dev','/run')
|
||||
or not (target.startswith(('/run/', '/tmp/', '/var/cache/')) or target == '/dev/shm')
|
||||
or '//' in target or target.endswith('/') or target in ('/etc','/usr','/bin','/lib','/lib64','/sbin','/proc','/sys','/dev')
|
||||
or target == '/run' and owner_strategy not in ('mapped-root', 'mapped-application-user', 'image-user')
|
||||
or not (target == '/run' or target.startswith(('/run/', '/tmp/', '/var/cache/')) or target == '/dev/shm')
|
||||
or isinstance(size, bool) or not isinstance(size, int) or size < 1):
|
||||
raise ValueError(translate('The tmpfs path or size is outside the supported profile'))
|
||||
if any(target == p or target.startswith(p+'/') or p.startswith(target+'/') for p in [*persistent,*targets]):
|
||||
@@ -43,11 +93,35 @@ def tmpfs_lines(deployment):
|
||||
raise ValueError(translate('Unsupported tmpfs options'))
|
||||
if len(options) != len(set(options)) or ('rw' in options and 'ro' in options):
|
||||
raise ValueError(translate('Contradictory tmpfs options'))
|
||||
mode = item.get('mode')
|
||||
if mode is not None:
|
||||
if not isinstance(mode, str) or not re.fullmatch(r'0[0-7]{3}', mode):
|
||||
raise ValueError(translate('Unsupported tmpfs mode'))
|
||||
if any(option.startswith('mode=') for option in options):
|
||||
raise ValueError(translate('The tmpfs mode is specified more than once'))
|
||||
if owner_strategy is not None:
|
||||
if owner_strategy == 'mapped-root':
|
||||
uid, gid = 0, 0
|
||||
elif owner_strategy == 'mapped-application-user':
|
||||
uid, gid = _application_ids(deployment, item)
|
||||
elif owner_strategy == 'image-user':
|
||||
uid, gid = _config_ids(config)
|
||||
else:
|
||||
raise ValueError(translate('Unsupported tmpfs owner strategy'))
|
||||
options = [*options, f'uid={uid}', f'gid={gid}']
|
||||
if mode is not None:
|
||||
options.append(f'mode={mode}')
|
||||
targets.add(target)
|
||||
result.append(f'tmpfs {target.lstrip("/")} tmpfs {",".join(options)},size={size}M,create=dir 0 0')
|
||||
return result
|
||||
|
||||
|
||||
def render_tmpfs(deployment_path, config_path):
|
||||
deployment = json.loads(Path(deployment_path).read_text())
|
||||
config = Path(config_path).read_bytes()
|
||||
return ''.join(f'lxc.mount.entry: {line}\n' for line in tmpfs_lines(deployment, config))
|
||||
|
||||
|
||||
# /etc/pve/lxc is the folder of the local node only; this one is the same on
|
||||
# every node of a cluster, so a migrated container still finds its include.
|
||||
CLUSTER_DIR = Path('/etc/pve/proxmenux')
|
||||
@@ -85,7 +159,7 @@ def seccomp_path(vmid):
|
||||
|
||||
|
||||
def check(config, deployment, vmid):
|
||||
expected = tmpfs_lines(deployment)
|
||||
expected = tmpfs_lines(deployment, config)
|
||||
lines = config.decode().splitlines()
|
||||
actual = [line.split(': ',1)[1] for line in lines if line.startswith('lxc.mount.entry: tmpfs ')]
|
||||
if sorted(actual) != sorted(expected):
|
||||
@@ -123,11 +197,11 @@ def file_binds(config):
|
||||
return found
|
||||
|
||||
|
||||
def filter_config(config, deployment):
|
||||
def filter_config(config, deployment, runtime_config=None):
|
||||
"""The configuration without the entries of the mounts the deployment
|
||||
declares itself: what is left belongs to the acceleration profile."""
|
||||
config = nested_mounts.without(config)
|
||||
declared = set(tmpfs_lines(deployment))
|
||||
declared = set(tmpfs_lines(deployment, runtime_config if runtime_config is not None else config))
|
||||
files = {m['source'] for m in deployment.get('mounts', []) if m.get('type') == 'host-bind'}
|
||||
|
||||
def own(line):
|
||||
@@ -141,7 +215,11 @@ def filter_config(config, deployment):
|
||||
|
||||
def check_recovery(config, state):
|
||||
plans = [state.get(key, {}).get('deployment', {}) for key in ('record', 'candidate_contract')]
|
||||
permitted = {line for plan in plans for line in tmpfs_lines(plan)}
|
||||
configs = [config]
|
||||
before_config = state.get('before_config')
|
||||
if isinstance(before_config, str):
|
||||
configs.append(before_config.encode())
|
||||
permitted = {line for plan in plans for source in configs for line in tmpfs_lines(plan, source)}
|
||||
for line in config.decode().splitlines():
|
||||
if line.startswith('lxc.mount.entry: tmpfs ') and line.split(': ', 1)[1] not in permitted:
|
||||
raise ValueError(translate('A tmpfs mount is not part of the journal; recovery blocked'))
|
||||
@@ -180,3 +258,11 @@ def restore(deployment, vmid):
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--render-tmpfs', nargs=2, metavar=('DEPLOYMENT', 'CONFIG'))
|
||||
args = parser.parse_args()
|
||||
if args.render_tmpfs:
|
||||
print(render_tmpfs(*args.render_tmpfs), end='')
|
||||
|
||||
@@ -578,8 +578,16 @@ def build_deployment(
|
||||
"container_path": item["container_path"],
|
||||
"size_mb": size_mb,
|
||||
"mount_options": item.get("mount_options", ["rw", "nosuid", "nodev"]),
|
||||
**({"owner_strategy": item["owner_strategy"]} if item.get("owner_strategy") else {}),
|
||||
**({"mode": item["mode"]} if item.get("mode") is not None else {}),
|
||||
**({"uid": item["uid"]} if item.get("uid") is not None else {}),
|
||||
**({"gid": item["gid"]} if item.get("gid") is not None else {}),
|
||||
}
|
||||
)
|
||||
if item.get("owner_strategy") == "mapped-application-user":
|
||||
for key in ("uid", "gid"):
|
||||
if key not in tmpfs_mounts[-1] and installer_profile.get("volume_owner", {}).get(key) is not None:
|
||||
tmpfs_mounts[-1][key] = installer_profile["volume_owner"][key]
|
||||
|
||||
if advanced:
|
||||
bridge = ask_bridge(ui, translate("Network bridge"), defaults["bridge"])
|
||||
|
||||
@@ -326,8 +326,16 @@ def refresh_template(candidate, ui):
|
||||
size = item['default_size_mb']
|
||||
if item.get('prompt_size', True):
|
||||
size = positive_integer(ui, f"{translate('Tmpfs size in MiB for')} {item['container_path']}", size, item.get('minimum_size_mb', 1))
|
||||
tmpfs.append({'container_path': item['container_path'], 'size_mb': size,
|
||||
'mount_options': copy.deepcopy(item.get('mount_options', ['rw', 'nosuid', 'nodev']))})
|
||||
tmpfs_mount = {'container_path': item['container_path'], 'size_mb': size,
|
||||
'mount_options': copy.deepcopy(item.get('mount_options', ['rw', 'nosuid', 'nodev']))}
|
||||
for key in ('owner_strategy', 'mode', 'uid', 'gid'):
|
||||
if item.get(key) is not None:
|
||||
tmpfs_mount[key] = copy.deepcopy(item[key])
|
||||
if item.get('owner_strategy') == 'mapped-application-user':
|
||||
for key in ('uid', 'gid'):
|
||||
if key not in tmpfs_mount and profile.get('volume_owner', {}).get(key) is not None:
|
||||
tmpfs_mount[key] = copy.deepcopy(profile['volume_owner'][key])
|
||||
tmpfs.append(tmpfs_mount)
|
||||
deployment['tmpfs_mounts'] = tmpfs
|
||||
sysctls = deployment.get('security', {}).get('sysctls', [])
|
||||
for item in profile.get('security', {}).get('sysctls', []):
|
||||
|
||||
@@ -53,10 +53,13 @@ class RuntimeHomeInstallerContractTests(unittest.TestCase):
|
||||
self.assertIn('set_runtime_environment HOME "$home"', self.source)
|
||||
|
||||
def test_home_fallback_runs_after_runtime_user_is_applied(self):
|
||||
self.assertIn(
|
||||
"apply_extra_hosts\napply_installer_profile\nensure_runtime_home\napply_rlimits",
|
||||
self.source,
|
||||
)
|
||||
profile = self.source.index("apply_installer_profile\n", self.source.index("apply_extra_hosts\n"))
|
||||
tmpfs = self.source.index("--render-tmpfs \"$DEPLOYMENT_FILE\" \"$CONF\"", profile)
|
||||
home = self.source.index("ensure_runtime_home\n", tmpfs)
|
||||
limits = self.source.index("apply_rlimits\n", home)
|
||||
self.assertLess(profile, tmpfs)
|
||||
self.assertLess(tmpfs, home)
|
||||
self.assertLess(home, limits)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Ownership and mode are preserved on volatile native OCI-LXC mounts."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'remote'))
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
|
||||
import oci_runtime_settings as runtime_settings
|
||||
|
||||
|
||||
class TmpfsOwnershipTests(unittest.TestCase):
|
||||
def test_image_user_is_taken_from_the_effective_lxc_init_identity(self):
|
||||
plan = {'tmpfs_mounts': [{
|
||||
'container_path': '/run', 'size_mb': 64, 'mount_options': ['rw', 'nosuid', 'nodev'],
|
||||
'owner_strategy': 'image-user', 'mode': '0755',
|
||||
}]}
|
||||
config = b'lxc.init.uid: 65534\nlxc.init.gid: 65534\n'
|
||||
self.assertEqual(runtime_settings.tmpfs_lines(plan, config), [
|
||||
'tmpfs run tmpfs rw,nosuid,nodev,uid=65534,gid=65534,mode=0755,size=64M,create=dir 0 0'
|
||||
])
|
||||
|
||||
def test_mapped_root_and_application_user_strategies(self):
|
||||
root = {'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'mapped-root'}]}
|
||||
app = {'environment': [{'name': 'PUID', 'value': '1001'}, {'name': 'PGID', 'value': '1002'}],
|
||||
'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'mapped-application-user'}]}
|
||||
overridden = {'environment': [{'name': 'PUID', 'value': '1001'}, {'name': 'PGID', 'value': '1002'}],
|
||||
'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'mapped-application-user',
|
||||
'uid': 2001, 'gid': 2002}]}
|
||||
self.assertIn('uid=0,gid=0', runtime_settings.tmpfs_lines(root)[0])
|
||||
self.assertIn('uid=1001,gid=1002', runtime_settings.tmpfs_lines(app)[0])
|
||||
self.assertIn('uid=2001,gid=2002', runtime_settings.tmpfs_lines(overridden)[0])
|
||||
|
||||
def test_plain_run_mount_is_rejected_without_an_owner_strategy(self):
|
||||
plan = {'tmpfs_mounts': [{'container_path': '/run', 'size_mb': 64,
|
||||
'mount_options': ['rw', 'nosuid', 'nodev']}]}
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.tmpfs_lines(plan)
|
||||
|
||||
def test_invalid_owner_ids_modes_and_duplicate_init_identity_are_rejected(self):
|
||||
app = {'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'mapped-application-user',
|
||||
'uid': 'root', 'gid': 10}]}
|
||||
mode = {'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'mapped-root', 'mode': '777'}]}
|
||||
image = {'tmpfs_mounts': [{'container_path': '/run/app', 'size_mb': 32,
|
||||
'mount_options': ['rw'], 'owner_strategy': 'image-user'}]}
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.tmpfs_lines(app)
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.tmpfs_lines(mode)
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.tmpfs_lines(image, b'lxc.init.uid: 1000\nlxc.init.uid: 1001\n')
|
||||
|
||||
def test_runtime_validation_requires_the_exact_rendered_owner_and_mode(self):
|
||||
plan = {'vmid': 120, 'tmpfs_mounts': [{
|
||||
'container_path': '/run', 'size_mb': 64, 'mount_options': ['rw'],
|
||||
'owner_strategy': 'image-user', 'mode': '0755',
|
||||
}]}
|
||||
entry = 'tmpfs run tmpfs rw,uid=1000,gid=1001,mode=0755,size=64M,create=dir 0 0'
|
||||
runtime_settings.check(
|
||||
f'lxc.init.uid: 1000\nlxc.init.gid: 1001\nlxc.mount.entry: {entry}\n'.encode(), plan, 120
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.check(
|
||||
f'lxc.init.uid: 1000\nlxc.init.gid: 1001\n'
|
||||
'lxc.mount.entry: tmpfs run tmpfs rw,uid=0,gid=0,mode=0755,size=64M,create=dir 0 0\n'.encode(),
|
||||
plan, 120,
|
||||
)
|
||||
|
||||
def test_acceleration_filter_recognises_image_owned_tmpfs_entry(self):
|
||||
plan = {'tmpfs_mounts': [{'container_path': '/run', 'size_mb': 64, 'mount_options': ['rw'],
|
||||
'owner_strategy': 'image-user', 'mode': '0755'}]}
|
||||
line = ('lxc.mount.entry: tmpfs run tmpfs rw,uid=1000,gid=1001,mode=0755,'
|
||||
'size=64M,create=dir 0 0\n')
|
||||
config = ('lxc.init.uid: 1000\nlxc.init.gid: 1001\n' + line).encode()
|
||||
filtered = runtime_settings.filter_config(config, plan).decode()
|
||||
self.assertNotIn('tmpfs', filtered)
|
||||
self.assertIn('lxc.init.uid: 1000', filtered)
|
||||
|
||||
def test_recovery_accepts_only_journalled_image_user_tmpfs_entries(self):
|
||||
plan = {'tmpfs_mounts': [{'container_path': '/run', 'size_mb': 64, 'mount_options': ['rw'],
|
||||
'owner_strategy': 'image-user', 'mode': '0755'}]}
|
||||
old = b'lxc.init.uid: 1000\nlxc.init.gid: 1001\n'
|
||||
current_line = ('lxc.mount.entry: tmpfs run tmpfs rw,uid=2000,gid=2001,mode=0755,'
|
||||
'size=64M,create=dir 0 0\n')
|
||||
current = ('lxc.init.uid: 2000\nlxc.init.gid: 2001\n' + current_line).encode()
|
||||
state = {'vmid': 120, 'before_config': old.decode(), 'record': {'deployment': plan},
|
||||
'candidate_contract': {'deployment': plan}}
|
||||
runtime_settings.check_recovery(current, state)
|
||||
unknown = current.replace(b'uid=2000,gid=2001', b'uid=2000,gid=3001')
|
||||
with self.assertRaises(ValueError):
|
||||
runtime_settings.check_recovery(unknown, state)
|
||||
|
||||
def test_input_is_visible_installable_and_uses_its_image_user_for_run(self):
|
||||
template = json.loads((ROOT / 'catalog/curated/input.json').read_text())
|
||||
profile = template['proxmox']['installer_profile']
|
||||
entry = next(item for item in profile['tmpfs_mounts'] if item['container_path'] == '/run')
|
||||
index = json.loads((ROOT / 'catalog/index.json').read_text())['applications']
|
||||
index_item = next(item for item in index if item['id'] == 'input')
|
||||
self.assertEqual(entry['owner_strategy'], 'image-user')
|
||||
self.assertEqual(entry['mode'], '0755')
|
||||
self.assertTrue(template['compatibility']['automatic_install_candidate'])
|
||||
self.assertTrue(index_item['automatic_install_candidate'])
|
||||
self.assertFalse(index_item['hidden'])
|
||||
self.assertFalse(template['compatibility']['validated'])
|
||||
|
||||
def test_installer_renders_tmpfs_after_the_profile_sets_the_image_identity(self):
|
||||
script = (ROOT / 'remote/install_oci.sh').read_text()
|
||||
profile = script.index('apply_installer_profile\n', script.index('prepare_managed_format_directories "/var/lib'))
|
||||
render = script.index('--render-tmpfs "$DEPLOYMENT_FILE" "$CONF"')
|
||||
self.assertLess(profile, render)
|
||||
|
||||
def test_recipe_renderer_uses_the_same_lines_as_runtime_validation(self):
|
||||
plan = {'tmpfs_mounts': [{'container_path': '/run', 'size_mb': 64, 'mount_options': ['rw'],
|
||||
'owner_strategy': 'image-user', 'mode': '0755'}]}
|
||||
config = b'lxc.init.uid: 65534\nlxc.init.gid: 65534\n'
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
deployment_path = Path(temporary) / 'deployment.json'
|
||||
config_path = Path(temporary) / 'container.conf'
|
||||
deployment_path.write_text(json.dumps(plan))
|
||||
config_path.write_bytes(config)
|
||||
self.assertEqual(runtime_settings.render_tmpfs(deployment_path, config_path),
|
||||
'lxc.mount.entry: ' + runtime_settings.tmpfs_lines(plan, config)[0] + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user