mirror of
https://github.com/MAGICGrants/truenas-apps.git
synced 2026-01-08 20:18:01 -05:00
Merge branch 'monero-app' into rework-monero
This commit is contained in:
496
catalog.json
496
catalog.json
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,7 @@ words:
|
||||
- freshclamd
|
||||
- freshrss
|
||||
- fscrawler
|
||||
- FTLCONF
|
||||
- ftpd
|
||||
- funcs
|
||||
- gandi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 2.19.3
|
||||
app_version: 2.19.4
|
||||
capabilities: []
|
||||
categories:
|
||||
- media
|
||||
@@ -33,4 +33,4 @@ sources:
|
||||
- https://github.com/advplyr/audiobookshelf
|
||||
title: Audiobookshelf
|
||||
train: community
|
||||
version: 1.3.17
|
||||
version: 1.3.18
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: ghcr.io/advplyr/audiobookshelf
|
||||
tag: 2.19.3
|
||||
tag: 2.19.4
|
||||
|
||||
consts:
|
||||
audiobookshelf_container_name: audiobookshelf
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 2.5.1
|
||||
app_version: 2.5.2
|
||||
capabilities: []
|
||||
categories:
|
||||
- financial
|
||||
@@ -30,4 +30,4 @@ sources:
|
||||
- https://www.chia.net/
|
||||
title: Chia
|
||||
train: community
|
||||
version: 1.1.9
|
||||
version: 1.1.10
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: ghcr.io/chia-network/chia
|
||||
tag: 2.5.1
|
||||
tag: 2.5.2
|
||||
|
||||
consts:
|
||||
chia_container_name: chia
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: v8.11.3
|
||||
app_version: v8.11.4
|
||||
capabilities: []
|
||||
categories:
|
||||
- monitoring
|
||||
@@ -26,4 +26,4 @@ sources:
|
||||
- https://github.com/amir20/dozzle
|
||||
title: Dozzle
|
||||
train: community
|
||||
version: 1.0.5
|
||||
version: 1.0.6
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: amir20/dozzle
|
||||
tag: v8.11.3
|
||||
tag: v8.11.4
|
||||
|
||||
consts:
|
||||
dozzle_container_name: dozzle
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 2024.12.4
|
||||
app_version: 2025.2.0
|
||||
capabilities:
|
||||
- description: ESPHome is able to create raw sockets, required for ICMP operations
|
||||
name: NET_RAW
|
||||
@@ -31,4 +31,4 @@ sources:
|
||||
- https://github.com/esphome/esphome
|
||||
title: ESPHome
|
||||
train: community
|
||||
version: 1.1.0
|
||||
version: 1.1.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: ghcr.io/esphome/esphome
|
||||
tag: 2024.12.4
|
||||
tag: 2025.2.0
|
||||
|
||||
consts:
|
||||
esphome_container_name: esphome
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: version-6.2.6
|
||||
app_version: version-6.2.7
|
||||
capabilities:
|
||||
- description: Firefly III and Firefly Data Importer is able to chown files.
|
||||
name: CHOWN
|
||||
@@ -57,4 +57,4 @@ sources:
|
||||
- https://github.com/firefly-iii/firefly-iii
|
||||
title: Firefly III
|
||||
train: community
|
||||
version: 1.5.8
|
||||
version: 1.5.9
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
images:
|
||||
image:
|
||||
repository: fireflyiii/core
|
||||
tag: version-6.2.6
|
||||
tag: version-6.2.7
|
||||
importer_image:
|
||||
repository: fireflyiii/data-importer
|
||||
tag: version-1.6.0
|
||||
tag: version-1.6.1
|
||||
postgres_15_image:
|
||||
repository: postgres
|
||||
tag: "15.11"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 1.23.3
|
||||
app_version: 1.23.4
|
||||
capabilities: []
|
||||
categories:
|
||||
- productivity
|
||||
@@ -38,4 +38,4 @@ sources:
|
||||
- https://docs.gitea.io/en-us/install-with-docker-rootless
|
||||
title: Gitea
|
||||
train: community
|
||||
version: 1.2.12
|
||||
version: 1.2.13
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: gitea/gitea
|
||||
tag: 1.23.3-rootless
|
||||
tag: 1.23.4-rootless
|
||||
postgres_15_image:
|
||||
repository: postgres
|
||||
tag: "15.11"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 11.5.1
|
||||
app_version: 11.5.2
|
||||
capabilities: []
|
||||
categories:
|
||||
- productivity
|
||||
@@ -34,4 +34,4 @@ sources:
|
||||
- https://github.com/grafana
|
||||
title: Grafana
|
||||
train: community
|
||||
version: 1.2.11
|
||||
version: 1.2.12
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: grafana/grafana
|
||||
tag: 11.5.1
|
||||
tag: 11.5.2
|
||||
|
||||
consts:
|
||||
grafana_container_name: grafana
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 3.13.1
|
||||
app_version: 3.13.2
|
||||
capabilities: []
|
||||
categories:
|
||||
- productivity
|
||||
@@ -28,4 +28,4 @@ sources:
|
||||
- https://app.iconik.io/help/pages/isg
|
||||
title: Iconik Storage Gateway
|
||||
train: community
|
||||
version: 1.0.12
|
||||
version: 1.0.13
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: ghcr.io/truenas/iconik-storage-gateway-docker
|
||||
tag: 3.13.1
|
||||
tag: 3.13.2
|
||||
|
||||
consts:
|
||||
iconik_container_name: iconik
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 5.11.40
|
||||
app_version: 5.11.41
|
||||
capabilities:
|
||||
- description: Invoice Ninja App, Worker and Scheduler are able to chown files.
|
||||
name: CHOWN
|
||||
@@ -63,4 +63,4 @@ sources:
|
||||
- https://github.com/invoiceninja/dockerfiles
|
||||
title: Invoice Ninja
|
||||
train: community
|
||||
version: 1.0.21
|
||||
version: 1.0.22
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: invoiceninja/invoiceninja-octane
|
||||
tag: "5.11.40-o"
|
||||
tag: "5.11.41-o"
|
||||
mariadb_image:
|
||||
repository: mariadb
|
||||
tag: "10.11.11"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 0.22.1438
|
||||
app_version: 0.22.1445
|
||||
capabilities: []
|
||||
categories:
|
||||
- media
|
||||
@@ -27,4 +27,4 @@ sources:
|
||||
- https://github.com/elfhosted/containers/tree/main/apps/jackett
|
||||
title: Jackett
|
||||
train: community
|
||||
version: 1.0.12
|
||||
version: 1.0.14
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: ghcr.io/elfhosted/jackett
|
||||
tag: 0.22.1438
|
||||
tag: 0.22.1445
|
||||
|
||||
consts:
|
||||
jackett_container_name: jackett
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 3.0.1-beta
|
||||
app_version: 3.3.2
|
||||
capabilities: []
|
||||
categories:
|
||||
- productivity
|
||||
@@ -36,4 +36,4 @@ sources:
|
||||
- https://hub.docker.com/r/joplin/server/
|
||||
title: Joplin
|
||||
train: community
|
||||
version: 1.3.8
|
||||
version: 1.3.9
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: joplin/server
|
||||
tag: 3.0.1-beta
|
||||
tag: 3.3.2-beta
|
||||
postgres_15_image:
|
||||
repository: postgres
|
||||
tag: "15.11"
|
||||
|
||||
4
ix-dev/community/kerberos-agent/README.md
Normal file
4
ix-dev/community/kerberos-agent/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Kerberos.io Agent
|
||||
|
||||
[Kerberos.io](https://kerberos.io/) is an open and scalable video surveillance system for anyone making this
|
||||
world a better and more peaceful place.
|
||||
34
ix-dev/community/kerberos-agent/app.yaml
Normal file
34
ix-dev/community/kerberos-agent/app.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
app_version: v3.3.10
|
||||
capabilities: []
|
||||
categories:
|
||||
- cameras
|
||||
description: An open and scalable video surveillance system for anyone making this
|
||||
world a better and more peaceful place.
|
||||
home: https://kerberos.io/
|
||||
host_mounts: []
|
||||
icon: https://media.sys.truenas.net/apps/kerberos-agent/icons/icon.svg
|
||||
keywords:
|
||||
- kerberos
|
||||
- security
|
||||
- video
|
||||
lib_version: 2.1.15
|
||||
lib_version_hash: e26cffb91766782c787867b379519f7407b1fed8450dce463cefa56340a41d84
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
url: https://www.truenas.com/
|
||||
name: kerberos-agent
|
||||
run_as_context:
|
||||
- description: Kerberos Agent runs as any non-root user.
|
||||
gid: 568
|
||||
group_name: kerberos-agent
|
||||
uid: 568
|
||||
user_name: kerberos-agent
|
||||
screenshots:
|
||||
- https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot1.png
|
||||
- https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot2.png
|
||||
sources:
|
||||
- https://github.com/kerberos-io/agent
|
||||
title: Kerberos.io Agent
|
||||
train: community
|
||||
version: 1.0.0
|
||||
10
ix-dev/community/kerberos-agent/item.yaml
Normal file
10
ix-dev/community/kerberos-agent/item.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
categories:
|
||||
- cameras
|
||||
icon_url: https://media.sys.truenas.net/apps/kerberos-agent/icons/icon.svg
|
||||
screenshots:
|
||||
- https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot1.png
|
||||
- https://media.sys.truenas.net/apps/kerberos-agent/screenshots/screenshot2.png
|
||||
tags:
|
||||
- kerberos
|
||||
- security
|
||||
- video
|
||||
9
ix-dev/community/kerberos-agent/ix_values.yaml
Normal file
9
ix-dev/community/kerberos-agent/ix_values.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
images:
|
||||
image:
|
||||
repository: kerberos/agent
|
||||
tag: v3.3.10
|
||||
|
||||
consts:
|
||||
kerberos_container_name: kerberos-agent
|
||||
perms_container_name: permissions
|
||||
config_container_name: config
|
||||
482
ix-dev/community/kerberos-agent/questions.yaml
Normal file
482
ix-dev/community/kerberos-agent/questions.yaml
Normal file
@@ -0,0 +1,482 @@
|
||||
groups:
|
||||
- name: Kerberos.io Agent Configuration
|
||||
description: Configure Kerberos.io Agent
|
||||
- name: Network Configuration
|
||||
description: Configure Network for Kerberos.io Agent
|
||||
- name: Storage Configuration
|
||||
description: Configure Storage for Kerberos.io Agent
|
||||
- name: Labels Configuration
|
||||
description: Configure Labels for Kerberos.io Agent
|
||||
- name: Resources Configuration
|
||||
description: Configure Resources for Kerberos.io Agent
|
||||
|
||||
questions:
|
||||
- variable: TZ
|
||||
group: Kerberos.io Agent Configuration
|
||||
label: Timezone
|
||||
schema:
|
||||
type: string
|
||||
default: Etc/UTC
|
||||
required: true
|
||||
$ref:
|
||||
- definitions/timezone
|
||||
- variable: Kerberos.io Agent
|
||||
label: ""
|
||||
group: Kerberos.io Agent Configuration
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: agent_name
|
||||
label: Agent Name
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: username
|
||||
label: Username
|
||||
description: The username for Kerberos.io Agent WebUI.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: password
|
||||
label: Password
|
||||
description: The password for Kerberos.io Agent WebUI.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
private: true
|
||||
- variable: additional_envs
|
||||
label: Additional Environment Variables
|
||||
description: Configure additional environment variables for Kerberos.io Agent.
|
||||
schema:
|
||||
type: list
|
||||
default: []
|
||||
items:
|
||||
- variable: env
|
||||
label: Environment Variable
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: name
|
||||
label: Name
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: value
|
||||
label: Value
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
- variable: network
|
||||
label: ""
|
||||
group: Network Configuration
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: web_port
|
||||
label: WebUI Port
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: bind_mode
|
||||
label: Port Bind Mode
|
||||
description: |
|
||||
The port bind mode.</br>
|
||||
- Publish: The port will be published on the host for external access.</br>
|
||||
- Expose: The port will be exposed for inter-container communication.</br>
|
||||
- None: The port will not be exposed or published.</br>
|
||||
Note: If the Dockerfile defines an EXPOSE directive,
|
||||
the port will still be exposed for inter-container communication regardless of this setting.
|
||||
schema:
|
||||
type: string
|
||||
default: "published"
|
||||
enum:
|
||||
- value: "published"
|
||||
description: Publish port on the host for external access
|
||||
- value: "exposed"
|
||||
description: Expose port for inter-container communication
|
||||
- value: ""
|
||||
description: None
|
||||
- variable: port_number
|
||||
label: Port Number
|
||||
schema:
|
||||
type: int
|
||||
show_if: [["bind_mode", "!=", ""]]
|
||||
default: 30122
|
||||
required: true
|
||||
$ref:
|
||||
- definitions/port
|
||||
- variable: host_ips
|
||||
label: Host IPs
|
||||
description: IPs on the host to bind this port
|
||||
schema:
|
||||
type: list
|
||||
default: []
|
||||
items:
|
||||
- variable: host_ip
|
||||
label: Host IP
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
$ref:
|
||||
- definitions/node_bind_ip
|
||||
- variable: storage
|
||||
label: ""
|
||||
group: Storage Configuration
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: config
|
||||
label: Config Storage
|
||||
description: The path to store config.
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: type
|
||||
label: Type
|
||||
description: |
|
||||
ixVolume: Is dataset created automatically by the system.</br>
|
||||
Host Path: Is a path that already exists on the system.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
immutable: true
|
||||
default: "ix_volume"
|
||||
enum:
|
||||
- value: "host_path"
|
||||
description: Host Path (Path that already exists on the system)
|
||||
- value: "ix_volume"
|
||||
description: ixVolume (Dataset created automatically by the system)
|
||||
- variable: ix_volume_config
|
||||
label: ixVolume Configuration
|
||||
description: The configuration for the ixVolume dataset.
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "ix_volume"]]
|
||||
$ref:
|
||||
- "normalize/ix_volume"
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: dataset_name
|
||||
label: Dataset Name
|
||||
description: The name of the dataset to use for storage.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
immutable: true
|
||||
hidden: true
|
||||
default: "config"
|
||||
- variable: acl_entries
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
- variable: host_path_config
|
||||
label: Host Path Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "host_path"]]
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: acl
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
$ref:
|
||||
- "normalize/acl"
|
||||
- variable: path
|
||||
label: Host Path
|
||||
description: The host path to use for storage.
|
||||
schema:
|
||||
type: hostpath
|
||||
show_if: [["acl_enable", "=", false]]
|
||||
required: true
|
||||
- variable: recordings
|
||||
label: Recordings Storage
|
||||
description: The path to store recordings.
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: type
|
||||
label: Type
|
||||
description: |
|
||||
ixVolume: Is dataset created automatically by the system.</br>
|
||||
Host Path: Is a path that already exists on the system.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
immutable: true
|
||||
default: "ix_volume"
|
||||
enum:
|
||||
- value: "host_path"
|
||||
description: Host Path (Path that already exists on the system)
|
||||
- value: "ix_volume"
|
||||
description: ixVolume (Dataset created automatically by the system)
|
||||
- variable: ix_volume_config
|
||||
label: ixVolume Configuration
|
||||
description: The configuration for the ixVolume dataset.
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "ix_volume"]]
|
||||
$ref:
|
||||
- "normalize/ix_volume"
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: dataset_name
|
||||
label: Dataset Name
|
||||
description: The name of the dataset to use for storage.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
immutable: true
|
||||
hidden: true
|
||||
default: "recordings"
|
||||
- variable: acl_entries
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
- variable: host_path_config
|
||||
label: Host Path Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "host_path"]]
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: acl
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
$ref:
|
||||
- "normalize/acl"
|
||||
- variable: path
|
||||
label: Host Path
|
||||
description: The host path to use for storage.
|
||||
schema:
|
||||
type: hostpath
|
||||
show_if: [["acl_enable", "=", false]]
|
||||
required: true
|
||||
- variable: additional_storage
|
||||
label: Additional Storage
|
||||
description: Additional storage for Kerberos.io Agent.
|
||||
schema:
|
||||
type: list
|
||||
default: []
|
||||
items:
|
||||
- variable: storageEntry
|
||||
label: Storage Entry
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: type
|
||||
label: Type
|
||||
description: |
|
||||
ixVolume: Is dataset created automatically by the system.</br>
|
||||
Host Path: Is a path that already exists on the system.</br>
|
||||
SMB Share: Is a SMB share that is mounted to as a volume.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
default: "ix_volume"
|
||||
immutable: true
|
||||
enum:
|
||||
- value: "host_path"
|
||||
description: Host Path (Path that already exists on the system)
|
||||
- value: "ix_volume"
|
||||
description: ixVolume (Dataset created automatically by the system)
|
||||
- value: "cifs"
|
||||
description: SMB/CIFS Share (Mounts a volume to a SMB share)
|
||||
- variable: read_only
|
||||
label: Read Only
|
||||
description: Mount the volume as read only.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: mount_path
|
||||
label: Mount Path
|
||||
description: The path inside the container to mount the storage.
|
||||
schema:
|
||||
type: path
|
||||
required: true
|
||||
- variable: host_path_config
|
||||
label: Host Path Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "host_path"]]
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: acl
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
$ref:
|
||||
- "normalize/acl"
|
||||
- variable: path
|
||||
label: Host Path
|
||||
description: The host path to use for storage.
|
||||
schema:
|
||||
type: hostpath
|
||||
show_if: [["acl_enable", "=", false]]
|
||||
required: true
|
||||
- variable: ix_volume_config
|
||||
label: ixVolume Configuration
|
||||
description: The configuration for the ixVolume dataset.
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "ix_volume"]]
|
||||
$ref:
|
||||
- "normalize/ix_volume"
|
||||
attrs:
|
||||
- variable: acl_enable
|
||||
label: Enable ACL
|
||||
description: Enable ACL for the storage.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- variable: dataset_name
|
||||
label: Dataset Name
|
||||
description: The name of the dataset to use for storage.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
immutable: true
|
||||
default: "storage_entry"
|
||||
- variable: acl_entries
|
||||
label: ACL Configuration
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["acl_enable", "=", true]]
|
||||
attrs: []
|
||||
$ref:
|
||||
- "normalize/acl"
|
||||
- variable: cifs_config
|
||||
label: SMB Configuration
|
||||
description: The configuration for the SMB dataset.
|
||||
schema:
|
||||
type: dict
|
||||
show_if: [["type", "=", "cifs"]]
|
||||
attrs:
|
||||
- variable: server
|
||||
label: Server
|
||||
description: The server to mount the SMB share.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: path
|
||||
label: Path
|
||||
description: The path to mount the SMB share.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: username
|
||||
label: Username
|
||||
description: The username to use for the SMB share.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: password
|
||||
label: Password
|
||||
description: The password to use for the SMB share.
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
private: true
|
||||
- variable: domain
|
||||
label: Domain
|
||||
description: The domain to use for the SMB share.
|
||||
schema:
|
||||
type: string
|
||||
- variable: labels
|
||||
label: ""
|
||||
group: Labels Configuration
|
||||
schema:
|
||||
type: list
|
||||
default: []
|
||||
items:
|
||||
- variable: label
|
||||
label: Label
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: key
|
||||
label: Key
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: value
|
||||
label: Value
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- variable: containers
|
||||
label: Containers
|
||||
description: Containers where the label should be applied
|
||||
schema:
|
||||
type: list
|
||||
items:
|
||||
- variable: container
|
||||
label: Container
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
enum:
|
||||
- value: kerberos-agent
|
||||
description: kerberos-agent
|
||||
- variable: resources
|
||||
label: ""
|
||||
group: Resources Configuration
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: limits
|
||||
label: Limits
|
||||
schema:
|
||||
type: dict
|
||||
attrs:
|
||||
- variable: cpus
|
||||
label: CPUs
|
||||
description: CPUs limit for Kerberos.io Agent.
|
||||
schema:
|
||||
type: int
|
||||
default: 2
|
||||
required: true
|
||||
- variable: memory
|
||||
label: Memory (in MB)
|
||||
description: Memory limit for Kerberos.io Agent.
|
||||
schema:
|
||||
type: int
|
||||
default: 4096
|
||||
required: true
|
||||
@@ -0,0 +1,56 @@
|
||||
{% from "macros/config.sh" import config_script %}
|
||||
{% set tpl = ix_lib.base.render.Render(values) %}
|
||||
|
||||
{% set c1 = tpl.add_container(values.consts.kerberos_container_name, "image") %}
|
||||
{% set config = tpl.add_container(values.consts.config_container_name, "image") %}
|
||||
{% set perm_container = tpl.deps.perms(values.consts.perms_container_name) %}
|
||||
{% set perms_config = {"uid": values.run_as.user, "gid": values.run_as.group, "mode": "check"} %}
|
||||
|
||||
{% do config.healthcheck.disable() %}
|
||||
{% do config.remove_devices() %}
|
||||
{% do config.restart.set_policy("on-failure", 1) %}
|
||||
{% do config.deploy.resources.set_profile("low") %}
|
||||
{% do config.configs.add("config.sh", config_script(), "/config.sh", "0755") %}
|
||||
{% do config.set_user(values.run_as.user, values.run_as.group) %}
|
||||
{% do config.set_entrypoint(["/config.sh"]) %}
|
||||
|
||||
{% do c1.depends.add_dependency(values.consts.config_container_name, "service_completed_successfully") %}
|
||||
{% do c1.set_user(values.run_as.user, values.run_as.group) %}
|
||||
{% do c1.add_caps(["NET_BIND_SERVICE"]) %}
|
||||
{% do c1.set_entrypoint([
|
||||
"/home/agent/main",
|
||||
"-action", "run",
|
||||
"-port", values.network.web_port.port_number,
|
||||
]) %}
|
||||
|
||||
{% do c1.healthcheck.set_test("curl", {"port": values.network.web_port.port_number}) %}
|
||||
|
||||
{% do c1.environment.add_env("AGENT_NAME", values.kerberos.agent_name) %}
|
||||
{% do c1.environment.add_env("AGENT_TIMEZONE", values.TZ) %}
|
||||
{% do c1.environment.add_env("AGENT_USERNAME", values.kerberos.username) %}
|
||||
{% do c1.environment.add_env("AGENT_PASSWORD", values.kerberos.password) %}
|
||||
{% do c1.environment.add_user_envs(values.kerberos.additional_envs) %}
|
||||
|
||||
{% do c1.add_port(values.network.web_port) %}
|
||||
|
||||
{% do c1.add_storage("/home/agent/data/recordings", values.storage.recordings) %}
|
||||
{% do perm_container.add_or_skip_action("recordings", values.storage.recordings, perms_config) %}
|
||||
|
||||
{% do c1.add_storage("/home/agent/data/config", values.storage.config) %}
|
||||
{% do config.add_storage("/home/agent/data/config", values.storage.config) %}
|
||||
{% do perm_container.add_or_skip_action("config", values.storage.config, perms_config) %}
|
||||
|
||||
{% for store in values.storage.additional_storage %}
|
||||
{% do c1.add_storage(store.mount_path, store) %}
|
||||
{% do perm_container.add_or_skip_action(store.mount_path, store, perms_config) %}
|
||||
{% endfor %}
|
||||
|
||||
{% if perm_container.has_actions() %}
|
||||
{% do perm_container.activate() %}
|
||||
{% do c1.depends.add_dependency(values.consts.perms_container_name, "service_completed_successfully") %}
|
||||
{% do config.depends.add_dependency(values.consts.perms_container_name, "service_completed_successfully") %}
|
||||
{% endif %}
|
||||
|
||||
{% do tpl.portals.add_portal({"port": values.network.web_port.port_number}) %}
|
||||
|
||||
{{ tpl.render() | tojson }}
|
||||
@@ -0,0 +1,418 @@
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
from storage import IxStorage
|
||||
|
||||
try:
|
||||
from .configs import ContainerConfigs
|
||||
from .depends import Depends
|
||||
from .deploy import Deploy
|
||||
from .device_cgroup_rules import DeviceCGroupRules
|
||||
from .devices import Devices
|
||||
from .dns import Dns
|
||||
from .environment import Environment
|
||||
from .error import RenderError
|
||||
from .expose import Expose
|
||||
from .extra_hosts import ExtraHosts
|
||||
from .formatter import escape_dollar, get_image_with_hashed_data
|
||||
from .healthcheck import Healthcheck
|
||||
from .labels import Labels
|
||||
from .ports import Ports
|
||||
from .restart import RestartPolicy
|
||||
from .validations import (
|
||||
valid_cap_or_raise,
|
||||
valid_ipc_mode_or_raise,
|
||||
valid_network_mode_or_raise,
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from .security_opts import SecurityOpts
|
||||
from .storage import Storage
|
||||
from .sysctls import Sysctls
|
||||
except ImportError:
|
||||
from configs import ContainerConfigs
|
||||
from depends import Depends
|
||||
from deploy import Deploy
|
||||
from device_cgroup_rules import DeviceCGroupRules
|
||||
from devices import Devices
|
||||
from dns import Dns
|
||||
from environment import Environment
|
||||
from error import RenderError
|
||||
from expose import Expose
|
||||
from extra_hosts import ExtraHosts
|
||||
from formatter import escape_dollar, get_image_with_hashed_data
|
||||
from healthcheck import Healthcheck
|
||||
from labels import Labels
|
||||
from ports import Ports
|
||||
from restart import RestartPolicy
|
||||
from validations import (
|
||||
valid_cap_or_raise,
|
||||
valid_ipc_mode_or_raise,
|
||||
valid_network_mode_or_raise,
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from security_opts import SecurityOpts
|
||||
from storage import Storage
|
||||
from sysctls import Sysctls
|
||||
|
||||
|
||||
class Container:
|
||||
def __init__(self, render_instance: "Render", name: str, image: str):
|
||||
self._render_instance = render_instance
|
||||
|
||||
self._name: str = name
|
||||
self._image: str = self._resolve_image(image)
|
||||
self._build_image: str = ""
|
||||
self._pull_policy: str = ""
|
||||
self._user: str = ""
|
||||
self._tty: bool = False
|
||||
self._stdin_open: bool = False
|
||||
self._init: bool | None = None
|
||||
self._read_only: bool | None = None
|
||||
self._extra_hosts: ExtraHosts = ExtraHosts(self._render_instance)
|
||||
self._hostname: str = ""
|
||||
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
|
||||
self._cap_add: set[str] = set()
|
||||
self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
|
||||
self._privileged: bool = False
|
||||
self._group_add: set[int | str] = set()
|
||||
self._network_mode: str = ""
|
||||
self._entrypoint: list[str] = []
|
||||
self._command: list[str] = []
|
||||
self._grace_period: int | None = None
|
||||
self._shm_size: int | None = None
|
||||
self._storage: Storage = Storage(self._render_instance)
|
||||
self._ipc_mode: str | None = None
|
||||
self._device_cgroup_rules: DeviceCGroupRules = DeviceCGroupRules(self._render_instance)
|
||||
self.sysctls: Sysctls = Sysctls(self._render_instance, self)
|
||||
self.configs: ContainerConfigs = ContainerConfigs(self._render_instance, self._render_instance.configs)
|
||||
self.deploy: Deploy = Deploy(self._render_instance)
|
||||
self.networks: set[str] = set()
|
||||
self.devices: Devices = Devices(self._render_instance)
|
||||
self.environment: Environment = Environment(self._render_instance, self.deploy.resources)
|
||||
self.dns: Dns = Dns(self._render_instance)
|
||||
self.depends: Depends = Depends(self._render_instance)
|
||||
self.healthcheck: Healthcheck = Healthcheck(self._render_instance)
|
||||
self.labels: Labels = Labels(self._render_instance)
|
||||
self.restart: RestartPolicy = RestartPolicy(self._render_instance)
|
||||
self.ports: Ports = Ports(self._render_instance)
|
||||
self.expose: Expose = Expose(self._render_instance)
|
||||
|
||||
self._auto_set_network_mode()
|
||||
self._auto_add_labels()
|
||||
self._auto_add_groups()
|
||||
|
||||
def _auto_add_groups(self):
|
||||
self.add_group(568)
|
||||
|
||||
def _auto_set_network_mode(self):
|
||||
if self._render_instance.values.get("network", {}).get("host_network", False):
|
||||
self.set_network_mode("host")
|
||||
|
||||
def _auto_add_labels(self):
|
||||
labels = self._render_instance.values.get("labels", [])
|
||||
if not labels:
|
||||
return
|
||||
|
||||
for label in labels:
|
||||
containers = label.get("containers", [])
|
||||
if not containers:
|
||||
raise RenderError(f'Label [{label.get("key", "")}] must have at least one container')
|
||||
|
||||
if self._name in containers:
|
||||
self.labels.add_label(label["key"], label["value"])
|
||||
|
||||
def _resolve_image(self, image: str):
|
||||
images = self._render_instance.values["images"]
|
||||
if image not in images:
|
||||
raise RenderError(
|
||||
f"Image [{image}] not found in values. " f"Available images: [{', '.join(images.keys())}]"
|
||||
)
|
||||
repo = images[image].get("repository", "")
|
||||
tag = images[image].get("tag", "")
|
||||
|
||||
if not repo:
|
||||
raise RenderError(f"Repository not found for image [{image}]")
|
||||
if not tag:
|
||||
raise RenderError(f"Tag not found for image [{image}]")
|
||||
|
||||
return f"{repo}:{tag}"
|
||||
|
||||
def build_image(self, content: list[str | None]):
|
||||
dockerfile = f"FROM {self._image}\n"
|
||||
for line in content:
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("FROM"):
|
||||
# TODO: This will also block multi-stage builds
|
||||
# We can revisit this later if we need it
|
||||
raise RenderError(
|
||||
"FROM cannot be used in build image. Define the base image when creating the container."
|
||||
)
|
||||
dockerfile += line + "\n"
|
||||
|
||||
self._build_image = dockerfile
|
||||
self._image = get_image_with_hashed_data(self._image, dockerfile)
|
||||
|
||||
def set_pull_policy(self, pull_policy: str):
|
||||
self._pull_policy = valid_pull_policy_or_raise(pull_policy)
|
||||
|
||||
def set_user(self, user: int, group: int):
|
||||
for i in (user, group):
|
||||
if not isinstance(i, int) or i < 0:
|
||||
raise RenderError(f"User/Group [{i}] is not valid")
|
||||
self._user = f"{user}:{group}"
|
||||
|
||||
def add_extra_host(self, host: str, ip: str):
|
||||
self._extra_hosts.add_host(host, ip)
|
||||
|
||||
def add_group(self, group: int | str):
|
||||
if isinstance(group, str):
|
||||
group = str(group).strip()
|
||||
if group.isdigit():
|
||||
raise RenderError(f"Group is a number [{group}] but passed as a string")
|
||||
|
||||
if group in self._group_add:
|
||||
raise RenderError(f"Group [{group}] already added")
|
||||
self._group_add.add(group)
|
||||
|
||||
def get_additional_groups(self) -> list[int | str]:
|
||||
result = []
|
||||
if self.deploy.resources.has_gpus() or self.devices.has_gpus():
|
||||
result.append(44) # video
|
||||
result.append(107) # render
|
||||
return result
|
||||
|
||||
def get_current_groups(self) -> list[str]:
|
||||
result = [str(g) for g in self._group_add]
|
||||
result.extend([str(g) for g in self.get_additional_groups()])
|
||||
return result
|
||||
|
||||
def set_tty(self, enabled: bool = False):
|
||||
self._tty = enabled
|
||||
|
||||
def set_stdin(self, enabled: bool = False):
|
||||
self._stdin_open = enabled
|
||||
|
||||
def set_ipc_mode(self, ipc_mode: str):
|
||||
self._ipc_mode = valid_ipc_mode_or_raise(ipc_mode, self._render_instance.container_names())
|
||||
|
||||
def add_device_cgroup_rule(self, dev_grp_rule: str):
|
||||
self._device_cgroup_rules.add_rule(dev_grp_rule)
|
||||
|
||||
def set_init(self, enabled: bool = False):
|
||||
self._init = enabled
|
||||
|
||||
def set_read_only(self, enabled: bool = False):
|
||||
self._read_only = enabled
|
||||
|
||||
def set_hostname(self, hostname: str):
|
||||
self._hostname = hostname
|
||||
|
||||
def set_grace_period(self, grace_period: int):
|
||||
if grace_period < 0:
|
||||
raise RenderError(f"Grace period [{grace_period}] cannot be negative")
|
||||
self._grace_period = grace_period
|
||||
|
||||
def set_privileged(self, enabled: bool = False):
|
||||
self._privileged = enabled
|
||||
|
||||
def clear_caps(self):
|
||||
self._cap_add.clear()
|
||||
self._cap_drop.clear()
|
||||
|
||||
def add_caps(self, caps: list[str]):
|
||||
for c in caps:
|
||||
if c in self._cap_add:
|
||||
raise RenderError(f"Capability [{c}] already added")
|
||||
self._cap_add.add(valid_cap_or_raise(c))
|
||||
|
||||
def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._security_opt.add_opt(key, value, arg)
|
||||
|
||||
def remove_security_opt(self, key: str):
|
||||
self._security_opt.remove_opt(key)
|
||||
|
||||
def set_network_mode(self, mode: str):
|
||||
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
|
||||
|
||||
def add_port(self, port_config: dict | None = None, dev_config: dict | None = None):
|
||||
port_config = port_config or {}
|
||||
dev_config = dev_config or {}
|
||||
# Merge port_config and dev_config (dev_config has precedence)
|
||||
config = port_config | dev_config
|
||||
|
||||
bind_mode = valid_port_bind_mode_or_raise(config.get("bind_mode", ""))
|
||||
# Skip port if its neither published nor exposed
|
||||
if not bind_mode:
|
||||
return
|
||||
|
||||
# Collect port config
|
||||
host_port = config.get("port_number", 0)
|
||||
container_port = config.get("container_port", 0) or host_port
|
||||
protocol = config.get("protocol", "tcp")
|
||||
host_ips = config.get("host_ips") or ["0.0.0.0", "::"]
|
||||
if not isinstance(host_ips, list):
|
||||
raise RenderError(f"Expected [host_ips] to be a list, got [{host_ips}]")
|
||||
|
||||
if bind_mode == "published":
|
||||
for host_ip in host_ips:
|
||||
self.ports.add_port(host_port, container_port, {"protocol": protocol, "host_ip": host_ip})
|
||||
elif bind_mode == "exposed":
|
||||
self.expose.add_port(container_port, protocol)
|
||||
|
||||
def set_entrypoint(self, entrypoint: list[str]):
|
||||
self._entrypoint = [escape_dollar(str(e)) for e in entrypoint]
|
||||
|
||||
def set_command(self, command: list[str]):
|
||||
self._command = [escape_dollar(str(e)) for e in command]
|
||||
|
||||
def add_storage(self, mount_path: str, config: "IxStorage"):
|
||||
self._storage.add(mount_path, config)
|
||||
|
||||
def add_docker_socket(self, read_only: bool = True, mount_path: str = "/var/run/docker.sock"):
|
||||
self.add_group(999)
|
||||
self._storage._add_docker_socket(read_only, mount_path)
|
||||
|
||||
def add_udev(self, read_only: bool = True, mount_path: str = "/run/udev"):
|
||||
self._storage._add_udev(read_only, mount_path)
|
||||
|
||||
def add_tun_device(self):
|
||||
self.devices._add_tun_device()
|
||||
|
||||
def add_snd_device(self):
|
||||
self.add_group(29)
|
||||
self.devices._add_snd_device()
|
||||
|
||||
def set_shm_size_mb(self, size: int):
|
||||
self._shm_size = size
|
||||
|
||||
# Easily remove devices from the container
|
||||
# Useful in dependencies like postgres and redis
|
||||
# where there is no need to pass devices to them
|
||||
def remove_devices(self):
|
||||
self.deploy.resources.remove_devices()
|
||||
self.devices.remove_devices()
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
return self._storage
|
||||
|
||||
def render(self) -> dict[str, Any]:
|
||||
if self._network_mode and self.networks:
|
||||
raise RenderError("Cannot set both [network_mode] and [networks]")
|
||||
|
||||
result = {
|
||||
"image": self._image,
|
||||
"platform": "linux/amd64",
|
||||
"tty": self._tty,
|
||||
"stdin_open": self._stdin_open,
|
||||
"restart": self.restart.render(),
|
||||
}
|
||||
|
||||
if self._pull_policy:
|
||||
result["pull_policy"] = self._pull_policy
|
||||
|
||||
if self.healthcheck.has_healthcheck():
|
||||
result["healthcheck"] = self.healthcheck.render()
|
||||
|
||||
if self._hostname:
|
||||
result["hostname"] = self._hostname
|
||||
|
||||
if self._build_image:
|
||||
result["build"] = {"tags": [self._image], "dockerfile_inline": self._build_image}
|
||||
|
||||
if self.configs.has_configs():
|
||||
result["configs"] = self.configs.render()
|
||||
|
||||
if self._ipc_mode is not None:
|
||||
result["ipc"] = self._ipc_mode
|
||||
|
||||
if self._device_cgroup_rules.has_rules():
|
||||
result["device_cgroup_rules"] = self._device_cgroup_rules.render()
|
||||
|
||||
if self._extra_hosts.has_hosts():
|
||||
result["extra_hosts"] = self._extra_hosts.render()
|
||||
|
||||
if self._init is not None:
|
||||
result["init"] = self._init
|
||||
|
||||
if self._read_only is not None:
|
||||
result["read_only"] = self._read_only
|
||||
|
||||
if self._grace_period is not None:
|
||||
result["stop_grace_period"] = f"{self._grace_period}s"
|
||||
|
||||
if self._user:
|
||||
result["user"] = self._user
|
||||
|
||||
for g in self.get_additional_groups():
|
||||
self.add_group(g)
|
||||
|
||||
if self._group_add:
|
||||
result["group_add"] = sorted(self._group_add, key=lambda g: (isinstance(g, str), g))
|
||||
|
||||
if self._shm_size is not None:
|
||||
result["shm_size"] = f"{self._shm_size}M"
|
||||
|
||||
if self._privileged is not None:
|
||||
result["privileged"] = self._privileged
|
||||
|
||||
if self._cap_drop:
|
||||
result["cap_drop"] = sorted(self._cap_drop)
|
||||
|
||||
if self._cap_add:
|
||||
result["cap_add"] = sorted(self._cap_add)
|
||||
|
||||
if self._security_opt.has_opts():
|
||||
result["security_opt"] = self._security_opt.render()
|
||||
|
||||
if self._network_mode:
|
||||
result["network_mode"] = self._network_mode
|
||||
|
||||
if self.sysctls.has_sysctls():
|
||||
result["sysctls"] = self.sysctls.render()
|
||||
|
||||
if self._network_mode != "host":
|
||||
if self.ports.has_ports():
|
||||
result["ports"] = self.ports.render()
|
||||
|
||||
if self.expose.has_ports():
|
||||
result["expose"] = self.expose.render()
|
||||
|
||||
if self._entrypoint:
|
||||
result["entrypoint"] = self._entrypoint
|
||||
|
||||
if self._command:
|
||||
result["command"] = self._command
|
||||
|
||||
if self.devices.has_devices():
|
||||
result["devices"] = self.devices.render()
|
||||
|
||||
if self.deploy.has_deploy():
|
||||
result["deploy"] = self.deploy.render()
|
||||
|
||||
if self.environment.has_variables():
|
||||
result["environment"] = self.environment.render()
|
||||
|
||||
if self.labels.has_labels():
|
||||
result["labels"] = self.labels.render()
|
||||
|
||||
if self.dns.has_dns_nameservers():
|
||||
result["dns"] = self.dns.render_dns_nameservers()
|
||||
|
||||
if self.dns.has_dns_searches():
|
||||
result["dns_search"] = self.dns.render_dns_searches()
|
||||
|
||||
if self.dns.has_dns_opts():
|
||||
result["dns_opt"] = self.dns.render_dns_opts()
|
||||
|
||||
if self.depends.has_dependencies():
|
||||
result["depends_on"] = self.depends.render()
|
||||
|
||||
if self._storage.has_mounts():
|
||||
result["volumes"] = self._storage.render()
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .validations import valid_security_opt_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from validations import valid_security_opt_or_raise
|
||||
|
||||
|
||||
class SecurityOpt:
|
||||
def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._opt: str = valid_security_opt_or_raise(opt)
|
||||
self._value = str(value).lower() if isinstance(value, bool) else value
|
||||
self._arg: str | None = arg
|
||||
|
||||
def render(self):
|
||||
result = self._opt
|
||||
if self._value is not None:
|
||||
result = f"{result}={self._value}"
|
||||
if self._arg is not None:
|
||||
result = f"{result}:{self._arg}"
|
||||
return result
|
||||
|
||||
|
||||
class SecurityOpts:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._opts: dict[str, SecurityOpt] = dict()
|
||||
self.add_opt("no-new-privileges", True)
|
||||
|
||||
def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
|
||||
if key in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] already added")
|
||||
self._opts[key] = SecurityOpt(key, value, arg)
|
||||
|
||||
def remove_opt(self, key: str):
|
||||
if key not in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] not found")
|
||||
del self._opts[key]
|
||||
|
||||
def has_opts(self):
|
||||
return len(self._opts) > 0
|
||||
|
||||
def render(self):
|
||||
result = []
|
||||
for opt in sorted(self._opts.values(), key=lambda o: o._opt):
|
||||
result.append(opt.render())
|
||||
return result
|
||||
@@ -0,0 +1,396 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_empty_container_name(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container(" ", "test_image")
|
||||
|
||||
|
||||
def test_resolve_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["image"] == "nginx:latest"
|
||||
|
||||
|
||||
def test_missing_repo(mock_values):
|
||||
mock_values["images"]["test_image"]["repository"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_missing_tag(mock_values):
|
||||
mock_values["images"]["test_image"]["tag"] = ""
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "test_image")
|
||||
|
||||
|
||||
def test_non_existing_image(mock_values):
|
||||
render = Render(mock_values)
|
||||
with pytest.raises(Exception):
|
||||
render.add_container("test_container", "non_existing_image")
|
||||
|
||||
|
||||
def test_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_pull_policy("always")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["pull_policy"] == "always"
|
||||
|
||||
|
||||
def test_invalid_pull_policy(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
with pytest.raises(Exception):
|
||||
c1.set_pull_policy("invalid_policy")
|
||||
|
||||
|
||||
def test_clear_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.add_caps(["NET_ADMIN"])
|
||||
c1.clear_caps()
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert "cap_drop" not in output["services"]["test_container"]
|
||||
assert "cap_add" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_privileged(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_privileged(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["privileged"] is True
|
||||
|
||||
|
||||
def test_tty(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_tty(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["tty"] is True
|
||||
|
||||
|
||||
def test_init(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_init(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["init"] is True
|
||||
|
||||
|
||||
def test_read_only(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_read_only(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["read_only"] is True
|
||||
|
||||
|
||||
def test_stdin(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_stdin(True)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stdin_open"] is True
|
||||
|
||||
|
||||
def test_hostname(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_hostname("test_hostname")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["hostname"] == "test_hostname"
|
||||
|
||||
|
||||
def test_grace_period(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_grace_period(10)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["stop_grace_period"] == "10s"
|
||||
|
||||
|
||||
def test_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_user(1000, 1000)
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["user"] == "1000:1000"
|
||||
|
||||
|
||||
def test_invalid_user(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_user(-100, 1000)
|
||||
|
||||
|
||||
def test_add_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
c1.add_group("video")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 1000, "video"]
|
||||
|
||||
|
||||
def test_add_duplicate_group(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_group(1000)
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group(1000)
|
||||
|
||||
|
||||
def test_add_group_as_string(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_group("1000")
|
||||
|
||||
|
||||
def test_add_docker_socket(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_docker_socket()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["group_add"] == [568, 999]
|
||||
assert output["services"]["test_container"]["volumes"] == [
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/var/run/docker.sock",
|
||||
"target": "/var/run/docker.sock",
|
||||
"read_only": True,
|
||||
"bind": {
|
||||
"propagation": "rprivate",
|
||||
"create_host_path": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_snd_device(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_snd_device()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["devices"] == ["/dev/snd:/dev/snd"]
|
||||
assert output["services"]["test_container"]["group_add"] == [29, 568]
|
||||
|
||||
|
||||
def test_shm_size(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_shm_size_mb(10)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["shm_size"] == "10M"
|
||||
|
||||
|
||||
def test_valid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_caps(["ALL", "NET_ADMIN"])
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["cap_add"] == ["ALL", "NET_ADMIN"]
|
||||
assert output["services"]["test_container"]["cap_drop"] == ["ALL"]
|
||||
|
||||
|
||||
def test_add_duplicate_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["ALL", "NET_ADMIN", "NET_ADMIN"])
|
||||
|
||||
|
||||
def test_invalid_caps(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_caps(["invalid_cap"])
|
||||
|
||||
|
||||
def test_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("host")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_auto_network_mode_with_host_network(mock_values):
|
||||
mock_values["network"] = {"host_network": True}
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "host"
|
||||
|
||||
|
||||
def test_network_mode_with_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_network_mode("service:test_container")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["network_mode"] == "service:test_container"
|
||||
|
||||
|
||||
def test_network_mode_with_container_missing(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("service:missing_container")
|
||||
|
||||
|
||||
def test_invalid_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_network_mode("invalid_mode")
|
||||
|
||||
|
||||
def test_entrypoint(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_entrypoint(["/bin/bash", "-c", "echo hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["entrypoint"] == ["/bin/bash", "-c", "echo hello $$MY_ENV"]
|
||||
|
||||
|
||||
def test_command(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.set_command(["echo", "hello $MY_ENV"])
|
||||
c1.healthcheck.disable()
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["command"] == ["echo", "hello $$MY_ENV"]
|
||||
|
||||
|
||||
def test_add_ports(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published"})
|
||||
c1.add_port({"port_number": 8082, "container_port": 8080, "bind_mode": "published", "protocol": "udp"})
|
||||
c1.add_port({"port_number": 8083, "container_port": 8080, "bind_mode": "exposed"})
|
||||
c1.add_port({"port_number": 8084, "container_port": 8080, "bind_mode": ""})
|
||||
c1.add_port(
|
||||
{"port_number": 9091, "container_port": 9091, "bind_mode": "published"},
|
||||
{"container_port": 9092, "protocol": "udp"},
|
||||
)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["ports"] == [
|
||||
{"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"},
|
||||
{"published": 8082, "target": 8080, "protocol": "udp", "mode": "ingress"},
|
||||
{"published": 9091, "target": 9092, "protocol": "udp", "mode": "ingress"},
|
||||
]
|
||||
assert output["services"]["test_container"]["expose"] == ["8080/tcp"]
|
||||
|
||||
|
||||
def test_add_ports_with_invalid_host_ips(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": "invalid"})
|
||||
|
||||
|
||||
def test_add_ports_with_empty_host_ips(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_port({"port_number": 8081, "container_port": 8080, "bind_mode": "published", "host_ips": []})
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["ports"] == [
|
||||
{"published": 8081, "target": 8080, "protocol": "tcp", "mode": "ingress"}
|
||||
]
|
||||
|
||||
|
||||
def test_set_ipc_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_ipc_mode("host")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["ipc"] == "host"
|
||||
|
||||
|
||||
def test_set_ipc_empty_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.set_ipc_mode("")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["ipc"] == ""
|
||||
|
||||
|
||||
def test_set_ipc_mode_with_invalid_ipc_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_ipc_mode("invalid")
|
||||
|
||||
|
||||
def test_set_ipc_mode_with_container_ipc_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c2 = render.add_container("test_container2", "test_image")
|
||||
c2.healthcheck.disable()
|
||||
c1.set_ipc_mode("container:test_container2")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["ipc"] == "container:test_container2"
|
||||
|
||||
|
||||
def test_set_ipc_mode_with_container_ipc_mode_and_invalid_container(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.set_ipc_mode("container:invalid")
|
||||
@@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("apparmor", "unconfined")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == ["apparmor=unconfined", "no-new-privileges=true"]
|
||||
|
||||
|
||||
def test_add_duplicate_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges", True)
|
||||
|
||||
|
||||
def test_add_empty_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("", True)
|
||||
|
||||
|
||||
def test_remove_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert "security_opt" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_add_security_opt_boolean(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
c1.add_security_opt("no-new-privileges", False)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == ["no-new-privileges=false"]
|
||||
|
||||
|
||||
def test_add_security_opt_arg(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("label", "type", "svirt_apache_t")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == [
|
||||
"label=type:svirt_apache_t",
|
||||
"no-new-privileges=true",
|
||||
]
|
||||
|
||||
|
||||
def test_add_security_opt_with_invalid_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("invalid")
|
||||
|
||||
|
||||
def test_add_security_opt_with_opt_containing_value(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges=true")
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("apparmor:unconfined")
|
||||
@@ -0,0 +1,326 @@
|
||||
import re
|
||||
import ipaddress
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
|
||||
OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
|
||||
RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/"))
|
||||
RESTRICTED: tuple[Path, ...] = (
|
||||
Path("/mnt/.ix-apps"),
|
||||
Path("/data"),
|
||||
Path("/var/db"),
|
||||
Path("/root"),
|
||||
Path("/conf"),
|
||||
Path("/audit"),
|
||||
Path("/var/run/middleware"),
|
||||
Path("/home"),
|
||||
Path("/boot"),
|
||||
Path("/var/log"),
|
||||
)
|
||||
|
||||
|
||||
def valid_security_opt_or_raise(opt: str):
|
||||
if ":" in opt or "=" in opt:
|
||||
raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")
|
||||
valid_opts = ["apparmor", "no-new-privileges", "seccomp", "systempaths", "label"]
|
||||
if opt not in valid_opts:
|
||||
raise RenderError(f"Security Option [{opt}] is not valid. Valid options are: [{', '.join(valid_opts)}]")
|
||||
|
||||
return opt
|
||||
|
||||
|
||||
def valid_port_bind_mode_or_raise(status: str):
|
||||
valid_statuses = ("published", "exposed", "")
|
||||
if status not in valid_statuses:
|
||||
raise RenderError(f"Invalid port status [{status}]")
|
||||
return status
|
||||
|
||||
|
||||
def valid_pull_policy_or_raise(pull_policy: str):
|
||||
valid_policies = ("missing", "always", "never", "build")
|
||||
if pull_policy not in valid_policies:
|
||||
raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
|
||||
return pull_policy
|
||||
|
||||
|
||||
def valid_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
|
||||
valid_modes = ("", "host", "private", "shareable", "none")
|
||||
if ipc_mode in valid_modes:
|
||||
return ipc_mode
|
||||
if ipc_mode.startswith("container:"):
|
||||
if ipc_mode[10:] not in containers:
|
||||
raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
|
||||
return ipc_mode
|
||||
raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
|
||||
|
||||
|
||||
def valid_sysctl_or_raise(sysctl: str, host_network: bool):
|
||||
if not sysctl:
|
||||
raise RenderError("Sysctl cannot be empty")
|
||||
if host_network and sysctl.startswith("net."):
|
||||
raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
|
||||
|
||||
valid_sysctls = [
|
||||
"kernel.msgmax",
|
||||
"kernel.msgmnb",
|
||||
"kernel.msgmni",
|
||||
"kernel.sem",
|
||||
"kernel.shmall",
|
||||
"kernel.shmmax",
|
||||
"kernel.shmmni",
|
||||
"kernel.shm_rmid_forced",
|
||||
]
|
||||
# https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
|
||||
if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
|
||||
raise RenderError(
|
||||
f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
|
||||
)
|
||||
return sysctl
|
||||
|
||||
|
||||
def valid_redis_password_or_raise(password: str):
|
||||
forbidden_chars = [" ", "'", "#"]
|
||||
for char in forbidden_chars:
|
||||
if char in password:
|
||||
raise RenderError(f"Redis password cannot contain [{char}]")
|
||||
|
||||
|
||||
def valid_octal_mode_or_raise(mode: str):
|
||||
mode = str(mode)
|
||||
if not OCTAL_MODE_REGEX.match(mode):
|
||||
raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_host_path_propagation(propagation: str):
|
||||
valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
|
||||
if propagation not in valid_propagations:
|
||||
raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
|
||||
return propagation
|
||||
|
||||
|
||||
def valid_portal_scheme_or_raise(scheme: str):
|
||||
schemes = ("http", "https")
|
||||
if scheme not in schemes:
|
||||
raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
|
||||
return scheme
|
||||
|
||||
|
||||
def valid_port_or_raise(port: int):
|
||||
if port < 1 or port > 65535:
|
||||
raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
|
||||
return port
|
||||
|
||||
|
||||
def valid_ip_or_raise(ip: str):
|
||||
try:
|
||||
ipaddress.ip_address(ip)
|
||||
except ValueError:
|
||||
raise RenderError(f"Invalid IP address [{ip}]")
|
||||
return ip
|
||||
|
||||
|
||||
def valid_port_mode_or_raise(mode: str):
|
||||
modes = ("ingress", "host")
|
||||
if mode not in modes:
|
||||
raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
|
||||
return mode
|
||||
|
||||
|
||||
def valid_port_protocol_or_raise(protocol: str):
|
||||
protocols = ("tcp", "udp")
|
||||
if protocol not in protocols:
|
||||
raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
|
||||
return protocol
|
||||
|
||||
|
||||
def valid_depend_condition_or_raise(condition: str):
|
||||
valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
|
||||
if condition not in valid_conditions:
|
||||
raise RenderError(
|
||||
f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
def valid_cgroup_perm_or_raise(cgroup_perm: str):
|
||||
valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
|
||||
if cgroup_perm not in valid_cgroup_perms:
|
||||
raise RenderError(
|
||||
f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
|
||||
)
|
||||
return cgroup_perm
|
||||
|
||||
|
||||
def valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
|
||||
parts = dev_grp_rule.split(" ")
|
||||
if len(parts) != 3:
|
||||
raise RenderError(
|
||||
f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [<type> <major>:<minor> <permission>]"
|
||||
)
|
||||
|
||||
valid_types = ("a", "b", "c")
|
||||
if parts[0] not in valid_types:
|
||||
raise RenderError(
|
||||
f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
|
||||
f" but got [{parts[0]}]"
|
||||
)
|
||||
|
||||
major, minor = parts[1].split(":")
|
||||
for part in (major, minor):
|
||||
if part != "*" and not part.isdigit():
|
||||
raise RenderError(
|
||||
f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
|
||||
f" or [*] but got [{major}] and [{minor}]"
|
||||
)
|
||||
|
||||
valid_cgroup_perm_or_raise(parts[2])
|
||||
|
||||
return dev_grp_rule
|
||||
|
||||
|
||||
def allowed_dns_opt_or_raise(dns_opt: str):
|
||||
disallowed_dns_opts = []
|
||||
if dns_opt in disallowed_dns_opts:
|
||||
raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
|
||||
return dns_opt
|
||||
|
||||
|
||||
def valid_http_path_or_raise(path: str):
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def valid_fs_path_or_raise(path: str):
|
||||
# There is no reason to allow / as a path,
|
||||
# either on host or in a container side.
|
||||
if path == "/":
|
||||
raise RenderError(f"Path [{path}] cannot be [/]")
|
||||
path = _valid_path_or_raise(path)
|
||||
return path
|
||||
|
||||
|
||||
def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool:
|
||||
"""
|
||||
Validates that the given path (after resolving symlinks) is not
|
||||
one of the restricted paths or within those restricted directories.
|
||||
|
||||
Returns True if the path is allowed, False otherwise.
|
||||
"""
|
||||
# Resolve the path to avoid symlink bypasses
|
||||
real_path = Path(input_path).resolve()
|
||||
for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]:
|
||||
if real_path.is_relative_to(restricted):
|
||||
return False
|
||||
|
||||
return real_path not in RESTRICTED_IN
|
||||
|
||||
|
||||
def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False):
|
||||
if not is_allowed_path(path, is_ix_volume):
|
||||
raise RenderError(f"Path [{path}] is not allowed to be mounted.")
|
||||
return path
|
||||
|
||||
|
||||
def _valid_path_or_raise(path: str):
|
||||
if path == "":
|
||||
raise RenderError(f"Path [{path}] cannot be empty")
|
||||
if not path.startswith("/"):
|
||||
raise RenderError(f"Path [{path}] must start with /")
|
||||
if "//" in path:
|
||||
raise RenderError(f"Path [{path}] cannot contain [//]")
|
||||
return path
|
||||
|
||||
|
||||
def allowed_device_or_raise(path: str):
|
||||
disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"]
|
||||
if path in disallowed_devices:
|
||||
raise RenderError(f"Device [{path}] is not allowed to be manually added.")
|
||||
return path
|
||||
|
||||
|
||||
def valid_network_mode_or_raise(mode: str, containers: list[str]):
|
||||
valid_modes = ("host", "none")
|
||||
if mode in valid_modes:
|
||||
return mode
|
||||
|
||||
if mode.startswith("service:"):
|
||||
if mode[8:] not in containers:
|
||||
raise RenderError(f"Service [{mode[8:]}] not found")
|
||||
return mode
|
||||
|
||||
raise RenderError(
|
||||
f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
|
||||
)
|
||||
|
||||
|
||||
def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
|
||||
valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
|
||||
if policy not in valid_restart_policies:
|
||||
raise RenderError(
|
||||
f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
|
||||
)
|
||||
if policy != "on-failure" and maximum_retry_count != 0:
|
||||
raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
|
||||
|
||||
if maximum_retry_count < 0:
|
||||
raise RenderError("Maximum retry count must be a positive integer")
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def valid_cap_or_raise(cap: str):
|
||||
valid_policies = (
|
||||
"ALL",
|
||||
"AUDIT_CONTROL",
|
||||
"AUDIT_READ",
|
||||
"AUDIT_WRITE",
|
||||
"BLOCK_SUSPEND",
|
||||
"BPF",
|
||||
"CHECKPOINT_RESTORE",
|
||||
"CHOWN",
|
||||
"DAC_OVERRIDE",
|
||||
"DAC_READ_SEARCH",
|
||||
"FOWNER",
|
||||
"FSETID",
|
||||
"IPC_LOCK",
|
||||
"IPC_OWNER",
|
||||
"KILL",
|
||||
"LEASE",
|
||||
"LINUX_IMMUTABLE",
|
||||
"MAC_ADMIN",
|
||||
"MAC_OVERRIDE",
|
||||
"MKNOD",
|
||||
"NET_ADMIN",
|
||||
"NET_BIND_SERVICE",
|
||||
"NET_BROADCAST",
|
||||
"NET_RAW",
|
||||
"PERFMON",
|
||||
"SETFCAP",
|
||||
"SETGID",
|
||||
"SETPCAP",
|
||||
"SETUID",
|
||||
"SYS_ADMIN",
|
||||
"SYS_BOOT",
|
||||
"SYS_CHROOT",
|
||||
"SYS_MODULE",
|
||||
"SYS_NICE",
|
||||
"SYS_PACCT",
|
||||
"SYS_PTRACE",
|
||||
"SYS_RAWIO",
|
||||
"SYS_RESOURCE",
|
||||
"SYS_TIME",
|
||||
"SYS_TTY_CONFIG",
|
||||
"SYSLOG",
|
||||
"WAKE_ALARM",
|
||||
)
|
||||
|
||||
if cap not in valid_policies:
|
||||
raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
|
||||
|
||||
return cap
|
||||
12
ix-dev/community/kerberos-agent/templates/macros/config.sh
Normal file
12
ix-dev/community/kerberos-agent/templates/macros/config.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
{% macro config_script() -%}
|
||||
#!/bin/sh
|
||||
|
||||
if [ ! -f /home/agent/data/config/config.json ]; then
|
||||
echo "Fetching default config..."
|
||||
curl --output /home/agent/data/config/config.json https://raw.githubusercontent.com/kerberos-io/agent/master/machinery/data/config/config.json
|
||||
exit 0
|
||||
else
|
||||
echo "Config already exists. Skipping..."
|
||||
exit 0
|
||||
fi
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,38 @@
|
||||
resources:
|
||||
limits:
|
||||
cpus: 2.0
|
||||
memory: 4096
|
||||
|
||||
TZ: UTC
|
||||
|
||||
kerberos:
|
||||
agent_name: agent-1
|
||||
username: user
|
||||
password: pass
|
||||
additional_envs: []
|
||||
|
||||
run_as:
|
||||
user: 568
|
||||
group: 568
|
||||
|
||||
network:
|
||||
web_port:
|
||||
bind_mode: published
|
||||
port_number: 8080
|
||||
|
||||
ix_volumes:
|
||||
recordings: /opt/tests/mnt/kerberos/recordings
|
||||
config: /opt/tests/mnt/kerberos/config
|
||||
|
||||
storage:
|
||||
recordings:
|
||||
type: ix_volume
|
||||
ix_volume_config:
|
||||
dataset_name: recordings
|
||||
create_host_path: true
|
||||
config:
|
||||
type: ix_volume
|
||||
ix_volume_config:
|
||||
dataset_name: config
|
||||
create_host_path: true
|
||||
additional_storage: []
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: 1.19.1
|
||||
app_version: 1.20.0
|
||||
capabilities: []
|
||||
categories:
|
||||
- media
|
||||
@@ -31,4 +31,4 @@ sources:
|
||||
- https://hub.docker.com/r/gotson/komga
|
||||
title: Komga
|
||||
train: community
|
||||
version: 1.2.13
|
||||
version: 1.2.14
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: gotson/komga
|
||||
tag: 1.19.1
|
||||
tag: 1.20.0
|
||||
|
||||
consts:
|
||||
config_path: /config
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
app_version: '2025-02-07'
|
||||
app_version: '2025-02-20'
|
||||
capabilities: []
|
||||
categories:
|
||||
- media
|
||||
@@ -30,4 +30,4 @@ sources:
|
||||
- https://github.com/alexta69/metube
|
||||
title: MeTube
|
||||
train: community
|
||||
version: 1.2.20
|
||||
version: 1.2.21
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
images:
|
||||
image:
|
||||
repository: alexta69/metube
|
||||
tag: "2025-02-07"
|
||||
tag: "2025-02-20"
|
||||
|
||||
consts:
|
||||
metube_container_name: metube
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user