mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Merge pull request #528 from jcastro/feature/oci-input-review
Support owner-aware OCI tmpfs mounts and enable Input
This commit is contained in:
+2
-1
@@ -509,8 +509,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
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<!-- Generated from oci/catalog/verification.json by .github/scripts/oci_validation.py. Do not edit by hand. -->
|
||||
|
||||
43 of 514 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 2 by the community. The OCI installer shows them as verified, with a ✓ in the lists.
|
||||
43 of 515 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 2 by the community. The OCI installer shows them as verified, with a ✓ in the lists.
|
||||
|
||||
Each test describes the scenario that was run and names the image it ran on. It does not cover every possible configuration of the application, and a newer image has not been tested until someone reports it.
|
||||
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-input",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Input"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Self-hosted forms and workflows."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted form builder using its bundled SQLite configuration for a single-container setup. The application key is required to encrypt stored data."
|
||||
},
|
||||
"category": "business",
|
||||
"category_label": "Business & ERP",
|
||||
"author": "Deck9",
|
||||
"developer": "Deck9",
|
||||
"icon": "https://raw.githubusercontent.com/deck9/input/main/docs/public/input-icon.png",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://getinput.co/",
|
||||
"documentation": "https://github.com/deck9/input/blob/main/docs/quick-start.md",
|
||||
"repository": "https://github.com/deck9/input",
|
||||
"tips": [
|
||||
"Create your first account in the web interface. This profile uses the single-container SQLite quick start; production deployments may instead use MariaDB, Redis, and S3."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "Deck9",
|
||||
"repository": "https://github.com/deck9/input",
|
||||
"revision": "2b08ab3b6bd73f12a03beedc19c6a561afe283c0",
|
||||
"readme_raw_url": "https://github.com/deck9/input/blob/main/docs/quick-start.md",
|
||||
"image_repository_url": "https://github.com/deck9/input/pkgs/container/input",
|
||||
"readme_pushed_at": "",
|
||||
"compose_sha256": "2153fd285760e5c8cd84b00efaa73641effa274e035ba86aa0ae153e7c17b8af",
|
||||
"generated_at": "2026-10-26T06:30:00+00:00"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "input",
|
||||
"container_name": "input",
|
||||
"image": {
|
||||
"reference": "ghcr.io/deck9/input:main",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/deck9/input",
|
||||
"tag": "main",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "APP_URL",
|
||||
"example": "http://@CONTAINER_ADDRESS@:8080",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-documentation",
|
||||
"container_address": "http://@CONTAINER_ADDRESS@:8080"
|
||||
},
|
||||
{
|
||||
"name": "APP_KEY",
|
||||
"example": "",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/var/www/html/storage",
|
||||
"compose_source_example": "./storage",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8080,
|
||||
"published_example": 8080,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "{\n \"name\": \"input\",\n \"services\": {\n \"input\": {\n \"image\": \"ghcr.io/deck9/input:main\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"./storage:/var/www/html/storage\"\n ],\n \"environment\": {\n \"APP_URL\": \"http://@CONTAINER_ADDRESS@:8080\",\n \"APP_KEY\": \"\"\n }\n }\n }\n}"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "input",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "input",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "input",
|
||||
"image": "ghcr.io/deck9/input:main",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/deck9/input:main",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"8080:8080"
|
||||
],
|
||||
"volumes": [
|
||||
"./storage:/var/www/html/storage"
|
||||
],
|
||||
"environment": {
|
||||
"APP_URL": "http://@CONTAINER_ADDRESS@:8080",
|
||||
"APP_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "input"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "input-volume-0",
|
||||
"service": "input",
|
||||
"container_path": "/var/www/html/storage",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host path for Input data at /var/www/html/storage"
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"input"
|
||||
],
|
||||
"stop_order": [
|
||||
"input"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "app-key",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "input",
|
||||
"environment_variable": "APP_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Input",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 2048,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "imported-compose-source",
|
||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||
"native_lxc_behavior": "The source model is preserved and remains blocked until every service option has a reviewed native Proxmox mapping.",
|
||||
"reason": "Catalog import must not imply runtime compatibility.",
|
||||
"behavioral_impact": "No automatic installation before review.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "rolling-latest-image",
|
||||
"upstream_behavior": "A discovered Compose may pin a release tag or digest.",
|
||||
"native_lxc_behavior": "ProxMenux selects the same image repository with the latest tag for catalog installations.",
|
||||
"reason": "The automatic catalog intentionally offers rolling latest images; pinned versions belong to the future manual installer.",
|
||||
"behavioral_impact": "The installed release can be newer than the discovered Compose revision.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes selected container ports on the Docker host.",
|
||||
"native_lxc_behavior": "A reviewed native application will listen on its original container ports at a dedicated LXC address.",
|
||||
"reason": "A native LXC has its own address and does not require Docker port NAT.",
|
||||
"behavioral_impact": "Published ports are metadata; ProxMenux URLs use the matching container target port.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "compose-shm-size",
|
||||
"upstream_behavior": "Compose sets the size of the container /dev/shm tmpfs.",
|
||||
"native_lxc_behavior": "ProxMenux mounts a native LXC tmpfs at /dev/shm with the same requested capacity.",
|
||||
"reason": "The OCI image runs directly as an LXC and therefore needs the equivalent Proxmox mount entry.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-command",
|
||||
"upstream_behavior": "Compose replaces the image Cmd while retaining its Entrypoint.",
|
||||
"native_lxc_behavior": "ProxMenux reads the official OCI Entrypoint and combines it with the Compose command as the native LXC init command.",
|
||||
"reason": "Proxmox stores the effective OCI process as one entrypoint string.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-privileged-mode",
|
||||
"upstream_behavior": "Compose selects whether the container runs in privileged mode.",
|
||||
"native_lxc_behavior": "ProxMenux keeps the LXC unprivileged by default and exposes the broad Compose privilege request only as an explicit compatibility option.",
|
||||
"reason": "A Compose privilege request is source metadata, not proof that the image technically requires a privileged LXC.",
|
||||
"behavioral_impact": "A privileged LXC has weaker isolation from the Proxmox host.",
|
||||
"validation": "native-equivalent"
|
||||
},
|
||||
{
|
||||
"id": "compose-process-runtime",
|
||||
"upstream_behavior": "Compose can replace Entrypoint, User and WorkingDir and request an init process or interactive terminal.",
|
||||
"native_lxc_behavior": "ProxMenux applies the process overrides through native LXC init directives; lxc-init provides PID 1 supervision and the CT console provides terminal access.",
|
||||
"reason": "The OCI process must start with the same identity, command and working directory without Docker.",
|
||||
"behavioral_impact": "Compose stdin_open and tty become access through the Proxmox LXC console.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-healthcheck",
|
||||
"upstream_behavior": "Docker periodically executes the declared container healthcheck.",
|
||||
"native_lxc_behavior": "For a single-service LXC, ProxMenux translates HTTP localhost checks into a mandatory first-start service check.",
|
||||
"reason": "Proxmox has no persistent Docker health state, while the installer still must detect a failed first boot.",
|
||||
"behavioral_impact": "The check runs during installation rather than continuously after installation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-cpu-priority",
|
||||
"upstream_behavior": "Docker cpu_shares sets a relative scheduling weight with 1024 as its neutral value.",
|
||||
"native_lxc_behavior": "ProxMenux converts the relative weight to Proxmox cpuunits with 100 as its neutral value and lets the user review it.",
|
||||
"reason": "Both settings express relative CPU priority on different scales.",
|
||||
"behavioral_impact": "Rounding and Proxmox minimum limits can slightly change very low weights.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-resource-limits",
|
||||
"upstream_behavior": "mem_limit sets the memory ceiling; ulimits sets process soft/hard resource limits.",
|
||||
"native_lxc_behavior": "mem_limit supplies the editable Proxmox memory default, rounded up to MiB; ulimits maps to lxc.prlimit with -1 represented as unlimited.",
|
||||
"reason": "Use native Proxmox memory and LXC prlimits, without a wrapper or changing the host kernel configuration.",
|
||||
"behavioral_impact": "User-selected memory overrides Compose. Swap is a separate choice. Kernel restrictions still apply; nproc counts processes by real UID, not by container.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-identity",
|
||||
"upstream_behavior": "Compose can set hostname, MAC address, extra hosts and attach a service to Docker networks.",
|
||||
"native_lxc_behavior": "ProxMenux applies hostname and MAC to net0, writes additional host aliases into the LXC and uses its dedicated bridge connection for single-service networks.",
|
||||
"reason": "A dedicated LXC has its own network namespace and does not need a Docker bridge per service.",
|
||||
"behavioral_impact": "host-gateway resolves to the IPv4 address of the selected Proxmox bridge.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-mode",
|
||||
"upstream_behavior": "Docker host mode removes Docker network isolation; bridge and default use a Docker-managed network.",
|
||||
"native_lxc_behavior": "The OCI process uses the dedicated LXC network namespace directly, so host, bridge and default all listen on the LXC address without Docker NAT.",
|
||||
"reason": "The LXC is the application host and already has its own address and port namespace.",
|
||||
"behavioral_impact": "host means the LXC host, never the Proxmox host; this preserves Proxmox network isolation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-capabilities-and-sysctls",
|
||||
"upstream_behavior": "Compose can add Linux capabilities and set kernel parameters in the container network namespace.",
|
||||
"native_lxc_behavior": "ProxMenux validates requested capabilities against the native LXC capability set and writes namespaced network settings as lxc.sysctl directives.",
|
||||
"reason": "A native OCI-LXC already starts with the namespaced capability set; lxc.cap.keep would incorrectly discard unrelated required capabilities.",
|
||||
"behavioral_impact": "Host-global capabilities such as SYS_MODULE remain blocked until their host prerequisite is explicitly adapted.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "docker-engine-metadata",
|
||||
"upstream_behavior": "Compose labels annotate Docker objects and the json-file logging driver rotates Docker-managed logs.",
|
||||
"native_lxc_behavior": "Labels remain source metadata; Docker json-file settings are not applied because the OCI process runs directly under LXC.",
|
||||
"reason": "There is no Docker object or Docker json-file log behind a native OCI-LXC application.",
|
||||
"behavioral_impact": "Docker-only label consumers and Docker log-driver rotation do not exist in the native deployment.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-device-passthrough",
|
||||
"upstream_behavior": "Compose passes host character devices or requests an NVIDIA runtime GPU.",
|
||||
"native_lxc_behavior": "ProxMenux converts recognized device declarations to native Proxmox dev resources; NVIDIA profiles also inject compatible host driver libraries read-only.",
|
||||
"reason": "Native OCI-LXC does not execute Docker device or NVIDIA runtime hooks.",
|
||||
"behavioral_impact": "Hardware is exposed only after explicit user confirmation and host-path validation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-host-ipc",
|
||||
"upstream_behavior": "ipc: host shares the Docker host IPC namespace, commonly to avoid Docker's small default shared-memory allocation.",
|
||||
"native_lxc_behavior": "The application keeps the LXC IPC namespace and receives a configurable 1 GiB /dev/shm instead of sharing Proxmox host IPC.",
|
||||
"reason": "Processes in a single native LXC already share one IPC namespace; retaining isolation is safer than exposing host IPC.",
|
||||
"behavioral_impact": "The application cannot exchange IPC objects with processes on the Proxmox host.",
|
||||
"validation": "not-requested-by-compose"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/var/www/html/storage",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"generated_sensitive_environment": {
|
||||
"APP_KEY": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 16,
|
||||
"prompt": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"image",
|
||||
"restart",
|
||||
"ports",
|
||||
"volumes",
|
||||
"environment"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"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",
|
||||
"clean_install": "pending",
|
||||
"service_health": "pending",
|
||||
"restart_persistence": "pending",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
{
|
||||
"schema_version": "0.5.0",
|
||||
"kind": "proxmenux.oci-template",
|
||||
"id": "image-input",
|
||||
"status": "generated-unvalidated",
|
||||
"catalog_ui": {
|
||||
"title": {
|
||||
"en_US": "Input"
|
||||
},
|
||||
"tagline": {
|
||||
"en_US": "Self-hosted forms and workflows."
|
||||
},
|
||||
"description": {
|
||||
"en_US": "A self-hosted form builder using its bundled SQLite configuration for a single-container setup. The application key is required to encrypt stored data."
|
||||
},
|
||||
"category": "business",
|
||||
"category_label": "Business & ERP",
|
||||
"author": "Deck9",
|
||||
"developer": "Deck9",
|
||||
"icon": "https://raw.githubusercontent.com/deck9/input/main/docs/public/input-icon.png",
|
||||
"thumbnail": null,
|
||||
"screenshots": [],
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"launch": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/"
|
||||
},
|
||||
"website": "https://getinput.co/",
|
||||
"documentation": "https://github.com/deck9/input/blob/main/docs/quick-start.md",
|
||||
"repository": "https://github.com/deck9/input",
|
||||
"tips": [
|
||||
"Create your first account in the web interface. This profile uses the single-container SQLite quick start; production deployments may instead use MariaDB, Redis, and S3."
|
||||
],
|
||||
"mini_changelog": [],
|
||||
"display_version": null,
|
||||
"updated_at": null
|
||||
},
|
||||
"source": {
|
||||
"provider": "Deck9",
|
||||
"repository": "https://github.com/deck9/input",
|
||||
"revision": "2b08ab3b6bd73f12a03beedc19c6a561afe283c0",
|
||||
"readme_raw_url": "https://github.com/deck9/input/blob/main/docs/quick-start.md",
|
||||
"image_repository_url": "https://github.com/deck9/input/pkgs/container/input",
|
||||
"readme_pushed_at": "",
|
||||
"compose_sha256": "2153fd285760e5c8cd84b00efaa73641effa274e035ba86aa0ae153e7c17b8af",
|
||||
"generated_at": "2026-10-26T06:30:00+00:00"
|
||||
},
|
||||
"container_contract": {
|
||||
"service_name": "input",
|
||||
"container_name": "input",
|
||||
"image": {
|
||||
"reference": "ghcr.io/deck9/input:main",
|
||||
"registry": "ghcr.io",
|
||||
"repository": "ghcr.io/deck9/input",
|
||||
"tag": "main",
|
||||
"digest": null,
|
||||
"pull_policy": "resolve-written-reference-to-architecture-digest-at-install"
|
||||
},
|
||||
"environment": [
|
||||
{
|
||||
"name": "APP_URL",
|
||||
"example": "http://@CONTAINER_ADDRESS@:8080",
|
||||
"required": true,
|
||||
"sensitive": false,
|
||||
"source": "upstream-documentation",
|
||||
"container_address": "http://@CONTAINER_ADDRESS@:8080"
|
||||
},
|
||||
{
|
||||
"name": "APP_KEY",
|
||||
"example": "",
|
||||
"required": true,
|
||||
"sensitive": true,
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"id": "volume-0",
|
||||
"container_path": "/var/www/html/storage",
|
||||
"compose_source_example": "./storage",
|
||||
"read_only": false,
|
||||
"required": true,
|
||||
"installation_choice": [
|
||||
"managed-volume",
|
||||
"host-bind"
|
||||
],
|
||||
"default": "managed-volume",
|
||||
"managed_volume": {
|
||||
"backup": true,
|
||||
"default_size_gb": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8080,
|
||||
"published_example": 8080,
|
||||
"protocol": "tcp",
|
||||
"required": true,
|
||||
"proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat"
|
||||
}
|
||||
],
|
||||
"related_services": [],
|
||||
"restart": "unless-stopped",
|
||||
"stop_grace_period": null,
|
||||
"original_compose": "{\n \"name\": \"input\",\n \"services\": {\n \"input\": {\n \"image\": \"ghcr.io/deck9/input:main\",\n \"restart\": \"unless-stopped\",\n \"ports\": [\n \"8080:8080\"\n ],\n \"volumes\": [\n \"./storage:/var/www/html/storage\"\n ],\n \"environment\": {\n \"APP_URL\": \"http://@CONTAINER_ADDRESS@:8080\",\n \"APP_KEY\": \"\"\n }\n }\n }\n}"
|
||||
},
|
||||
"compose_stack": {
|
||||
"project_name": "input",
|
||||
"deployment_model": "one-native-oci-lxc-per-compose-service",
|
||||
"user_experience": "single-application-install",
|
||||
"main_service": "input",
|
||||
"service_count": 1,
|
||||
"services": [
|
||||
{
|
||||
"name": "input",
|
||||
"image": "ghcr.io/deck9/input:main",
|
||||
"is_main": true,
|
||||
"role": "frontend",
|
||||
"vmid_offset": 0,
|
||||
"depends_on": [],
|
||||
"frontend_network": true,
|
||||
"private_network": false,
|
||||
"compose": {
|
||||
"image": "ghcr.io/deck9/input:main",
|
||||
"restart": "unless-stopped",
|
||||
"ports": [
|
||||
"8080:8080"
|
||||
],
|
||||
"volumes": [
|
||||
"./storage:/var/www/html/storage"
|
||||
],
|
||||
"environment": {
|
||||
"APP_URL": "http://@CONTAINER_ADDRESS@:8080",
|
||||
"APP_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"top_level": {
|
||||
"name": "input"
|
||||
},
|
||||
"networking": {
|
||||
"frontend": "selected-proxmox-bridge",
|
||||
"private_required": false,
|
||||
"private_creation": "automatic-create-if-missing",
|
||||
"private_address_allocation": "automatic-static-address-per-service",
|
||||
"service_discovery": "private-addresses-with-compose-service-host-aliases",
|
||||
"dependency_external_access": "disabled-unless-service-publishes-ports",
|
||||
"prompt_user_for_private_network": false
|
||||
},
|
||||
"storage": [
|
||||
{
|
||||
"id": "input-volume-0",
|
||||
"service": "input",
|
||||
"container_path": "/var/www/html/storage",
|
||||
"mode": "managed-volume",
|
||||
"user_selectable": true,
|
||||
"backup": true,
|
||||
"shared_with_other_lxc": false,
|
||||
"source_path": null,
|
||||
"source_path_prompt": "Host path for Input data at /var/www/html/storage"
|
||||
}
|
||||
],
|
||||
"orchestration": {
|
||||
"reserve_vmids_atomically": 1,
|
||||
"start_order": [
|
||||
"input"
|
||||
],
|
||||
"stop_order": [
|
||||
"input"
|
||||
],
|
||||
"dependency_readiness": "compose-healthcheck-then-port-or-process-fallback",
|
||||
"rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes"
|
||||
},
|
||||
"installer_inputs": {
|
||||
"prompted": [
|
||||
"stack_name",
|
||||
"base_vmid",
|
||||
"rootfs_storage",
|
||||
"persistent_data_destinations",
|
||||
"frontend_bridge",
|
||||
"frontend_ipv4_mode"
|
||||
],
|
||||
"automatic": [
|
||||
"dependent_vmids",
|
||||
"private_bridge",
|
||||
"private_subnet",
|
||||
"private_service_addresses",
|
||||
"compose_service_aliases",
|
||||
"generated_secrets",
|
||||
"dependency_start_and_stop_order"
|
||||
],
|
||||
"generated_secrets": [
|
||||
{
|
||||
"id": "app-key",
|
||||
"strategy": "generate-cryptographically-random-at-install",
|
||||
"bindings": [
|
||||
{
|
||||
"service": "input",
|
||||
"environment_variable": "APP_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"first_run": {
|
||||
"endpoints": [
|
||||
{
|
||||
"label": "Input",
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"source": "upstream-documentation"
|
||||
}
|
||||
],
|
||||
"credentials": []
|
||||
},
|
||||
"proxmox": {
|
||||
"runtime": "native-oci-lxc",
|
||||
"technology_status": "proxmox-technology-preview",
|
||||
"defaults": {
|
||||
"unprivileged": true,
|
||||
"ostype": "auto-from-image",
|
||||
"cores": 2,
|
||||
"memory_mb": 2048,
|
||||
"swap_mb": 512,
|
||||
"rootfs_size_gb": 8,
|
||||
"rootfs_storage": "local-lvm",
|
||||
"volume_storage": "local-lvm",
|
||||
"template_storage": "local",
|
||||
"bridge": "vmbr0",
|
||||
"ipv4": "dhcp",
|
||||
"firewall": true,
|
||||
"host_managed_network": true,
|
||||
"onboot": false,
|
||||
"features": [
|
||||
"nesting=1"
|
||||
],
|
||||
"shutdown_timeout_seconds": 30
|
||||
},
|
||||
"image_metadata_policy": {
|
||||
"entrypoint": "import-from-oci-image",
|
||||
"cmd": "import-from-oci-image",
|
||||
"environment": "import-image-env-then-apply-compose-overrides",
|
||||
"user": "import-from-oci-image",
|
||||
"working_dir": "import-from-oci-image",
|
||||
"stop_signal": "import-from-oci-image"
|
||||
},
|
||||
"adaptations": [
|
||||
{
|
||||
"id": "imported-compose-source",
|
||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||
"native_lxc_behavior": "The source model is preserved and remains blocked until every service option has a reviewed native Proxmox mapping.",
|
||||
"reason": "Catalog import must not imply runtime compatibility.",
|
||||
"behavioral_impact": "No automatic installation before review.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "rolling-latest-image",
|
||||
"upstream_behavior": "A discovered Compose may pin a release tag or digest.",
|
||||
"native_lxc_behavior": "ProxMenux selects the same image repository with the latest tag for catalog installations.",
|
||||
"reason": "The automatic catalog intentionally offers rolling latest images; pinned versions belong to the future manual installer.",
|
||||
"behavioral_impact": "The installed release can be newer than the discovered Compose revision.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "dedicated-lxc-network",
|
||||
"upstream_behavior": "Docker publishes selected container ports on the Docker host.",
|
||||
"native_lxc_behavior": "A reviewed native application will listen on its original container ports at a dedicated LXC address.",
|
||||
"reason": "A native LXC has its own address and does not require Docker port NAT.",
|
||||
"behavioral_impact": "Published ports are metadata; ProxMenux URLs use the matching container target port.",
|
||||
"validation": "pending-per-application"
|
||||
},
|
||||
{
|
||||
"id": "compose-shm-size",
|
||||
"upstream_behavior": "Compose sets the size of the container /dev/shm tmpfs.",
|
||||
"native_lxc_behavior": "ProxMenux mounts a native LXC tmpfs at /dev/shm with the same requested capacity.",
|
||||
"reason": "The OCI image runs directly as an LXC and therefore needs the equivalent Proxmox mount entry.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-command",
|
||||
"upstream_behavior": "Compose replaces the image Cmd while retaining its Entrypoint.",
|
||||
"native_lxc_behavior": "ProxMenux reads the official OCI Entrypoint and combines it with the Compose command as the native LXC init command.",
|
||||
"reason": "Proxmox stores the effective OCI process as one entrypoint string.",
|
||||
"behavioral_impact": "None expected.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-privileged-mode",
|
||||
"upstream_behavior": "Compose selects whether the container runs in privileged mode.",
|
||||
"native_lxc_behavior": "ProxMenux keeps the LXC unprivileged by default and exposes the broad Compose privilege request only as an explicit compatibility option.",
|
||||
"reason": "A Compose privilege request is source metadata, not proof that the image technically requires a privileged LXC.",
|
||||
"behavioral_impact": "A privileged LXC has weaker isolation from the Proxmox host.",
|
||||
"validation": "native-equivalent"
|
||||
},
|
||||
{
|
||||
"id": "compose-process-runtime",
|
||||
"upstream_behavior": "Compose can replace Entrypoint, User and WorkingDir and request an init process or interactive terminal.",
|
||||
"native_lxc_behavior": "ProxMenux applies the process overrides through native LXC init directives; lxc-init provides PID 1 supervision and the CT console provides terminal access.",
|
||||
"reason": "The OCI process must start with the same identity, command and working directory without Docker.",
|
||||
"behavioral_impact": "Compose stdin_open and tty become access through the Proxmox LXC console.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-healthcheck",
|
||||
"upstream_behavior": "Docker periodically executes the declared container healthcheck.",
|
||||
"native_lxc_behavior": "For a single-service LXC, ProxMenux translates HTTP localhost checks into a mandatory first-start service check.",
|
||||
"reason": "Proxmox has no persistent Docker health state, while the installer still must detect a failed first boot.",
|
||||
"behavioral_impact": "The check runs during installation rather than continuously after installation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-cpu-priority",
|
||||
"upstream_behavior": "Docker cpu_shares sets a relative scheduling weight with 1024 as its neutral value.",
|
||||
"native_lxc_behavior": "ProxMenux converts the relative weight to Proxmox cpuunits with 100 as its neutral value and lets the user review it.",
|
||||
"reason": "Both settings express relative CPU priority on different scales.",
|
||||
"behavioral_impact": "Rounding and Proxmox minimum limits can slightly change very low weights.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-resource-limits",
|
||||
"upstream_behavior": "mem_limit sets the memory ceiling; ulimits sets process soft/hard resource limits.",
|
||||
"native_lxc_behavior": "mem_limit supplies the editable Proxmox memory default, rounded up to MiB; ulimits maps to lxc.prlimit with -1 represented as unlimited.",
|
||||
"reason": "Use native Proxmox memory and LXC prlimits, without a wrapper or changing the host kernel configuration.",
|
||||
"behavioral_impact": "User-selected memory overrides Compose. Swap is a separate choice. Kernel restrictions still apply; nproc counts processes by real UID, not by container.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-identity",
|
||||
"upstream_behavior": "Compose can set hostname, MAC address, extra hosts and attach a service to Docker networks.",
|
||||
"native_lxc_behavior": "ProxMenux applies hostname and MAC to net0, writes additional host aliases into the LXC and uses its dedicated bridge connection for single-service networks.",
|
||||
"reason": "A dedicated LXC has its own network namespace and does not need a Docker bridge per service.",
|
||||
"behavioral_impact": "host-gateway resolves to the IPv4 address of the selected Proxmox bridge.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-network-mode",
|
||||
"upstream_behavior": "Docker host mode removes Docker network isolation; bridge and default use a Docker-managed network.",
|
||||
"native_lxc_behavior": "The OCI process uses the dedicated LXC network namespace directly, so host, bridge and default all listen on the LXC address without Docker NAT.",
|
||||
"reason": "The LXC is the application host and already has its own address and port namespace.",
|
||||
"behavioral_impact": "host means the LXC host, never the Proxmox host; this preserves Proxmox network isolation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-capabilities-and-sysctls",
|
||||
"upstream_behavior": "Compose can add Linux capabilities and set kernel parameters in the container network namespace.",
|
||||
"native_lxc_behavior": "ProxMenux validates requested capabilities against the native LXC capability set and writes namespaced network settings as lxc.sysctl directives.",
|
||||
"reason": "A native OCI-LXC already starts with the namespaced capability set; lxc.cap.keep would incorrectly discard unrelated required capabilities.",
|
||||
"behavioral_impact": "Host-global capabilities such as SYS_MODULE remain blocked until their host prerequisite is explicitly adapted.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "docker-engine-metadata",
|
||||
"upstream_behavior": "Compose labels annotate Docker objects and the json-file logging driver rotates Docker-managed logs.",
|
||||
"native_lxc_behavior": "Labels remain source metadata; Docker json-file settings are not applied because the OCI process runs directly under LXC.",
|
||||
"reason": "There is no Docker object or Docker json-file log behind a native OCI-LXC application.",
|
||||
"behavioral_impact": "Docker-only label consumers and Docker log-driver rotation do not exist in the native deployment.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-device-passthrough",
|
||||
"upstream_behavior": "Compose passes host character devices or requests an NVIDIA runtime GPU.",
|
||||
"native_lxc_behavior": "ProxMenux converts recognized device declarations to native Proxmox dev resources; NVIDIA profiles also inject compatible host driver libraries read-only.",
|
||||
"reason": "Native OCI-LXC does not execute Docker device or NVIDIA runtime hooks.",
|
||||
"behavioral_impact": "Hardware is exposed only after explicit user confirmation and host-path validation.",
|
||||
"validation": "not-requested-by-compose"
|
||||
},
|
||||
{
|
||||
"id": "compose-host-ipc",
|
||||
"upstream_behavior": "ipc: host shares the Docker host IPC namespace, commonly to avoid Docker's small default shared-memory allocation.",
|
||||
"native_lxc_behavior": "The application keeps the LXC IPC namespace and receives a configurable 1 GiB /dev/shm instead of sharing Proxmox host IPC.",
|
||||
"reason": "Processes in a single native LXC already share one IPC namespace; retaining isolation is safer than exposing host IPC.",
|
||||
"behavioral_impact": "The application cannot exchange IPC objects with processes on the Proxmox host.",
|
||||
"validation": "not-requested-by-compose"
|
||||
}
|
||||
],
|
||||
"installer_profile": {
|
||||
"volume_preparations": [
|
||||
{
|
||||
"container_path": "/var/www/html/storage",
|
||||
"remove_lost_found": true,
|
||||
"owner_strategy": "mapped-application-user",
|
||||
"only_when_mount_type": "managed-volume"
|
||||
}
|
||||
],
|
||||
"startup_healthcheck": {
|
||||
"scheme": "http",
|
||||
"port": 8080,
|
||||
"path": "/",
|
||||
"timeout_seconds": 180,
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"generated_sensitive_environment": {
|
||||
"APP_KEY": {
|
||||
"strategy": "token-hex",
|
||||
"bytes": 16,
|
||||
"prompt": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"compatibility": {
|
||||
"automatic_install_candidate": true,
|
||||
"validated": false,
|
||||
"supported_compose_keys": [
|
||||
"image",
|
||||
"restart",
|
||||
"ports",
|
||||
"volumes",
|
||||
"environment"
|
||||
],
|
||||
"untranslated_blockers": [],
|
||||
"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",
|
||||
"clean_install": "pending",
|
||||
"service_health": "pending",
|
||||
"restart_persistence": "pending",
|
||||
"backup_restore": "pending",
|
||||
"update_preserves_data": "pending"
|
||||
},
|
||||
"lifecycle": {
|
||||
"update_strategy": "resolve-latest-image-then-apply-reviewed-native-lxc-update",
|
||||
"registry_state": {
|
||||
"resolved_architecture": null,
|
||||
"resolved_digest": null,
|
||||
"image_version_label": null,
|
||||
"image_created": null
|
||||
},
|
||||
"change_detection": "compare-compose-sha256-and-resolved-latest-image-digest",
|
||||
"automatic_unattended_updates": false
|
||||
}
|
||||
}
|
||||
@@ -6646,6 +6646,38 @@
|
||||
"hidden": false,
|
||||
"template_id": "linuxserver-inkscape"
|
||||
},
|
||||
{
|
||||
"id": "input",
|
||||
"provider": "official",
|
||||
"template_family": "curated-profile",
|
||||
"title": "Input",
|
||||
"repository": "https://github.com/deck9/input",
|
||||
"description": "A self-hosted form builder using its bundled SQLite configuration for a single-container setup. The application key is required to encrypt stored data.",
|
||||
"website": "https://getinput.co/",
|
||||
"icon": "https://raw.githubusercontent.com/deck9/input/main/docs/public/input-icon.png",
|
||||
"architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"updated_at": null,
|
||||
"main_image": "ghcr.io/deck9/input:main",
|
||||
"category": "business",
|
||||
"category_label": "Business & ERP",
|
||||
"replaces_discovered_ids": [],
|
||||
"curated_path": "catalog/curated/input.json",
|
||||
"template": "apps/input.json",
|
||||
"template_status": "generated-unvalidated",
|
||||
"automatic_install_candidate": true,
|
||||
"untranslated_blockers": [],
|
||||
"requires_privileged_lxc": false,
|
||||
"optional_privileged_lxc": false,
|
||||
"requires_host_pid_namespace": false,
|
||||
"requires_relaxed_confinement": false,
|
||||
"optional_relaxed_confinement": false,
|
||||
"requires_security_confirmation": false,
|
||||
"hidden": false,
|
||||
"template_id": "image-input"
|
||||
},
|
||||
{
|
||||
"id": "instatic",
|
||||
"provider": "CoreBunch",
|
||||
|
||||
@@ -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