update lib and hashes

This commit is contained in:
Stavros kois
2024-06-25 21:49:27 +03:00
parent f43823c78e
commit 9bb63f41dd
13 changed files with 662 additions and 35 deletions

View File

@@ -1,50 +1,51 @@
app_version: "1.40.2.8395"
app_version: 1.40.2.8395
capabilities:
- name: CHOWN
description: Plex is able to chown files.
- name: FOWNER
description: Plex is able to bypass permission checks for it's sub-processes.
- name: DAC_OVERRIDE
description: Plex is able to bypass permission checks.
- name: SETGID
description: Plex is able to set group ID for it's sub-processes.
- name: SETUID
description: Plex is able to set user ID for it's sub-processes.
- name: KILL
description: Plex is able to kill processes.
- description: Plex is able to chown files.
name: CHOWN
- description: Plex is able to bypass permission checks for it's sub-processes.
name: FOWNER
- description: Plex is able to bypass permission checks.
name: DAC_OVERRIDE
- description: Plex is able to set group ID for it's sub-processes.
name: SETGID
- description: Plex is able to set user ID for it's sub-processes.
name: SETUID
- description: Plex is able to kill processes.
name: KILL
categories:
- media
description: Plex is a media server that allows you to stream your media to any Plex client.
- media
description: Plex is a media server that allows you to stream your media to any Plex
client.
home: https://plex.tv
host_mounts: []
icon: https://media.sys.truenas.net/apps/plex/icons/icon.png
keywords:
- plex
- media
- entertainment
- movies
- series
- tv
- streaming
- plex
- media
- entertainment
- movies
- series
- tv
- streaming
lib_version: 1.0.0
lib_version_hash: ""
lib_version_hash: 7275a78ff384fab4e8a7c41791d2e0b8e244cb71ed7956ccffffa9455d840da0
maintainers:
- email: dev@ixsystems.com
name: truenas
url: https://www.truenas.com/
- email: dev@ixsystems.com
name: truenas
url: https://www.truenas.com/
name: plex
run_as_context:
- description: Plex runs as root user.
user_name: root
uid: 0
group_name: root
gid: 0
- description: Plex runs as root user.
gid: 0
group_name: root
uid: 0
user_name: root
screenshots:
- https://media.sys.truenas.net/apps/plex/screenshots/screenshot1.png
- https://media.sys.truenas.net/apps/plex/screenshots/screenshot2.png
- https://media.sys.truenas.net/apps/plex/screenshots/screenshot1.png
- https://media.sys.truenas.net/apps/plex/screenshots/screenshot2.png
sources:
- https://plex.tv
- https://hub.docker.com/r/plexinc/pms-docker
- https://plex.tv
- https://hub.docker.com/r/plexinc/pms-docker
title: Plex
train: charts
version: 1.0.0

View File

@@ -0,0 +1,26 @@
from . import utils
def envs(app={}, user=[]):
track_env = {**app}
result = {**app}
if not user:
user = []
elif isinstance(user, dict):
user = [{"name": k, "value": v} for k, v in user.items()]
else:
utils.throw_error(f"Unsupported type for user environment variables [{type(user)}]")
for k in app.keys():
if not k:
utils.throw_error("Environment variable name cannot be empty.")
for item in user:
if not item.get("name", None):
utils.throw_error("Environment variable name cannot be empty.")
if item.get("name", None) in track_env:
utils.throw_error(f"Environment variable [{k}] is already defined from the application developer.")
track_env[item["name"]] = item.get("value", None)
result[item["name"]] = item.get("value", None)
return result

View File

@@ -0,0 +1,39 @@
from . import utils
def check_health(test, interval=10, timeout=10, retries=5, start_period=30):
if not test:
utils.throw_error("Expected [test] to be set")
return {
"test": test,
"interval": f"{interval}s",
"timeout": f"{timeout}s",
"retries": retries,
"start_period": f"{start_period}s",
}
def pg_test(user, db, host="127.0.0.1", port=5432):
if not user or not db:
utils.throw_error("Postgres container: [user] and [db] must be set")
return f"pg_isready -h {host} -p {port} -d {db} -U {user}"
def curl_test(url):
if not url:
utils.throw_error("Curl test: [url] must be set")
return f"curl --silent --output /dev/null --show-error --fail {url}"
def wget_test(url):
if not url:
utils.throw_error("Wget test: [url] must be set")
return f"wget --spider --quiet {url}"
def http_test(port, path, host="127.0.0.1"):
if not port or not path:
utils.throw_error("Expected [port] and [path] to be set")
return f"/bin/bash -c 'exec {{health_check_fd}}<>/dev/tcp/{host}/{port} && echo -e \"GET {path} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n\" >&$${{health_check_fd}} && cat <&$${{health_check_fd}}'"

View File

@@ -0,0 +1,18 @@
from . import utils
def dns_opts(dns_opts=[]):
if not dns_opts:
return []
tracked = {}
disallowed_opts = []
for opt in dns_opts:
key = opt.split(":")[0]
if key in tracked:
utils.throw_error(f"Expected [dns_opts] to be unique, got [{', '.join([d.split(':')[0] for d in tracked])}]")
if key in disallowed_opts:
utils.throw_error(f"Expected [dns_opts] to not contain [{key}] key.")
tracked[key] = opt
return dns_opts

View File

@@ -0,0 +1,40 @@
from . import utils
import ipaddress
def must_valid_port(num: int):
if num < 1 or num > 65535:
utils.throw_error(f"Expected a valid port number, got [{num}]")
def must_valid_ip(ip: str):
try:
ipaddress.ip_address(ip)
except ValueError:
utils.throw_error(f"Expected a valid IP address, got [{ip}]")
def must_valid_protocol(protocol: str):
if protocol not in ["tcp", "udp"]:
utils.throw_error(f"Expected a valid protocol, got [{protocol}]")
def must_valid_mode(mode: str):
if mode not in ["ingress", "host"]:
utils.throw_error(f"Expected a valid mode, got [{mode}]")
def get_port(port={}):
must_valid_port(port["published"])
must_valid_port(port["target"])
must_valid_ip(port.get("host_ip", "0.0.0.0"))
must_valid_protocol(port.get("protocol", "tcp"))
must_valid_mode(port.get("mode", "ingress"))
return {
"target": port["target"],
"published": port["published"],
"protocol": port.get("protocol", "tcp"),
"mode": port.get("mode", "ingress"),
"host_ip": port.get("host_ip", "0.0.0.0"),
}

View File

@@ -0,0 +1,34 @@
from . import utils
def pg_url(variant, host, user, password, dbname, port=5432):
if not host:
utils.throw_error("Expected [host] to be set")
if not user:
utils.throw_error("Expected [user] to be set")
if not password:
utils.throw_error("Expected [password] to be set")
if not dbname:
utils.throw_error("Expected [dbname] to be set")
if variant == "postgresql":
return f"postgresql://{user}:{password}@{host}:{port}/{dbname}?sslmode=disable"
elif variant == "postgres":
return f"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode=disable"
else:
utils.throw_error(f"Expected [variant] to be one of [postgresql, postgres], got [{variant}]")
def pg_env(user, password, dbname, port=5432):
if not user:
utils.throw_error("Expected [user] to be set for postgres")
if not password:
utils.throw_error("Expected [password] to be set for postgres")
if not dbname:
utils.throw_error("Expected [dbname] to be set for postgres")
return {
"POSTGRES_USER": user,
"POSTGRES_PASSWORD": password,
"POSTGRES_DB": dbname,
"POSTGRES_PORT": port,
}

View File

@@ -0,0 +1,18 @@
from . import utils
import re
def resources(data):
cpus = str(data.get("limits", {}).get("cpus", "2.0"))
memory = str(data.get("limits", {}).get("memory", "4gb"))
if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", cpus):
utils.throw_error(f"Expected cpus to be a number or a float, got [{cpus}]")
if not re.match(r"^[1-9][0-9]*(([GMK]B?)|([gmk]b?))$", memory):
raise ValueError(f"Expected memory to be a number with unit, got [{memory}]")
return {
"limits": {
"cpus": cpus,
"memory": memory,
},
}

View File

@@ -0,0 +1,16 @@
def get_caps(add=[], drop=[]):
result = {"drop": drop or ["ALL"]}
if add:
result["add"] = add
return result
def get_sec_opts(add=[], remove=[]):
result = ["no-new-privileges"]
for opt in add:
if opt not in result:
result.append(opt)
for opt in remove:
if opt in result:
result.remove(opt)
return result

View File

@@ -0,0 +1,303 @@
from . import utils
import re
BIND_TYPES = ["host_path", "ix_volume"]
VOL_TYPES = ["volume", "nfs", "cifs"]
ALL_TYPES = BIND_TYPES + VOL_TYPES + ["tmpfs", "anonymous"]
PROPAGATION_TYPES = ["shared", "slave", "private", "rshared", "rslave", "rprivate"]
# Basic validation for a path (Expand later)
def valid_path(path=""):
if not path.startswith("/"):
utils.throw_error(f"Expected path [{path}] to start with /")
# There is no reason to allow / as a path, either on host or in a container
if path == "/":
utils.throw_error(f"Expected path [{path}] to not be /")
return path
# Returns a volume mount object (Used in container's "volumes" level)
def vol_mount(data, ix_volumes=[]):
vol_type = _get_docker_vol_type(data)
volume = {
"type": vol_type,
"target": valid_path(data.get("mount_path", "")),
"read_only": data.get("read_only", False),
}
if vol_type == "bind": # Default create_host_path is true in short-syntax
volume.update(_get_bind_vol_config(data, ix_volumes))
elif vol_type == "volume":
volume.update(_get_volume_vol_config(data))
elif vol_type == "tmpfs":
volume.update(_get_tmpfs_vol_config(data))
elif vol_type == "anonymous":
volume["type"] = "volume"
volume.update(_get_anonymous_vol_config(data))
return volume
def storage_item(data, ix_volumes=[], perm_opts={}):
return {
"vol_mount": vol_mount(data, ix_volumes),
"vol": vol(data),
"perms_item": perms_item(data, ix_volumes, perm_opts) if perm_opts else {},
}
def perms_item(data, ix_volumes, opts={}):
if not data.get("auto_permissions"):
return {}
if data.get("type") == "host_path":
if data.get("host_path_config", {}).get("aclEnable", False):
return {}
if data.get("type") == "ix_volume":
if data.get("ix_volume_config", {}).get("aclEnable", False):
return {}
if not ix_volumes:
ix_volumes = []
req_keys = ["mount_path", "mode", "uid", "gid"]
for key in req_keys:
if not opts.get(key):
utils.throw_error(f"Expected opts passed to [perms_item] to have [{key}] key")
data.update({"mount_path": opts["mount_path"]})
volume_mount = vol_mount(data, ix_volumes)
return {
"vol_mount": volume_mount,
"perm_dir": {
"dir": volume_mount["target"],
"mode": opts["mode"],
"uid": opts["uid"],
"gid": opts["gid"],
"chmod": opts.get("chmod", ""),
},
}
def _get_bind_vol_config(data, ix_volumes=[]):
path = host_path(data, ix_volumes)
if data.get("propagation", "rprivate") not in PROPAGATION_TYPES:
utils.throw_error(f"Expected [propagation] to be one of [{', '.join(PROPAGATION_TYPES)}], got [{data['propagation']}]")
# https://docs.docker.com/storage/bind-mounts/#configure-bind-propagation
return {"source": path, "bind": {"create_host_path": data.get("host_path_config", {}).get("create_host_path", True), "propagation": _get_valid_propagation(data)}}
def _get_volume_vol_config(data):
if not data.get("volume_name"):
utils.throw_error("Expected [volume_name] to be set for [volume] type")
return {"source": data["volume_name"], "volume": _process_volume_config(data)}
def _get_anonymous_vol_config(data):
return {"volume": _process_volume_config(data)}
mode_regex = re.compile(r"^0[0-7]{3}$")
def _get_tmpfs_vol_config(data):
tmpfs = {}
config = data.get("tmpfs_config", {})
if config.get("size"):
if not isinstance(config["size"], int):
utils.throw_error("Expected [size] to be an integer for [tmpfs] type")
if not config["size"] > 0:
utils.throw_error("Expected [size] to be greater than 0 for [tmpfs] type")
# Convert Mebibytes to Bytes
tmpfs.update({"size": config["size"] * 1024 * 1024})
if config.get("mode"):
if not mode_regex.match(str(config["mode"])):
utils.throw_error(f"Expected [mode] to be a octal string for [tmpfs] type, got [{config['mode']}]")
tmpfs.update({"mode": int(config["mode"], 8)})
return {"tmpfs": tmpfs}
# Returns a volume object (Used in top "volumes" level)
def vol(data):
if not data or _get_docker_vol_type(data) != "volume":
return {}
if not data.get("volume_name"):
utils.throw_error("Expected [volume_name] to be set for [volume] type")
if data["type"] == "nfs":
return {data["volume_name"]: _process_nfs(data)}
elif data["type"] == "cifs":
return {data["volume_name"]: _process_cifs(data)}
else:
return {data["volume_name"]: {}}
def _is_host_path(data):
return data.get("type") == "host_path"
def _get_valid_propagation(data):
if not data.get("propagation"):
return "rprivate"
if not data["propagation"] in PROPAGATION_TYPES:
utils.throw_error(f"Expected [propagation] to be one of [{', '.join(PROPAGATION_TYPES)}], got [{data['propagation']}]")
return data["propagation"]
def _is_ix_volume(data):
return data.get("type") == "ix_volume"
# Returns the host path for a for either a host_path or ix_volume
def host_path(data, ix_volumes=[]):
path = ""
if _is_host_path(data):
path = _process_host_path_config(data)
elif _is_ix_volume(data):
path = _process_ix_volume_config(data, ix_volumes)
else:
utils.throw_error(f"Expected [_host_path] to be called only for types [host_path, ix_volume], got [{data['type']}]")
return valid_path(path)
# Returns the type of storage as used in docker-compose
def _get_docker_vol_type(data):
if not data.get("type"):
utils.throw_error("Expected [type] to be set for storage")
if data["type"] not in ALL_TYPES:
utils.throw_error(f"Expected storage [type] to be one of {ALL_TYPES}, got [{data['type']}]")
if data["type"] in BIND_TYPES:
return "bind"
elif data["type"] in VOL_TYPES:
return "volume"
else:
return data["type"]
def _process_host_path_config(data):
if data.get("host_path_config", {}).get("aclEnable", False):
if not data["host_path_config"].get("acl", {}).get("path"):
utils.throw_error("Expected [host_path_config.acl.path] to be set for [host_path] type with ACL enabled")
return data["host_path_config"]["acl"]["path"]
if not data.get("host_path_config", {}).get("path"):
utils.throw_error("Expected [host_path_config.path] to be set for [host_path] type")
return data["host_path_config"]["path"]
def _process_volume_config(data):
return {"nocopy": data.get("volume_config", {}).get("nocopy", False)}
def _process_ix_volume_config(data, ix_volumes):
path = ""
if not data.get("ix_volume_config", {}).get("dataset_name"):
utils.throw_error("Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type")
if not ix_volumes:
utils.throw_error("Expected [ix_volumes] to be set for [ix_volume] type")
ds = data["ix_volume_config"]["dataset_name"]
for item in ix_volumes:
# TODO: verify the "hostPath" key is the correct from middleware side
# Ideally we would want to have the "dataset_name" in the dict, instead of doing this check below
if item.get("hostPath", "").split("/")[-1] == ds:
path = item["hostPath"]
break
if not path:
utils.throw_error(f"Expected [ix_volumes] to contain path for dataset with name [{ds}]")
return path
# Constructs a volume object for a cifs type
def _process_cifs(data):
if not data.get("cifs_config"):
utils.throw_error("Expected [cifs_config] to be set for [cifs] type")
required_keys = ["server", "path", "username", "password"]
for key in required_keys:
if not data["cifs_config"].get(key):
utils.throw_error(f"Expected [{key}] to be set for [cifs] type")
opts = [f"user={data['cifs_config']['username']}", f"password={data['cifs_config']['password']}"]
if data["cifs_config"].get("options"):
if not isinstance(data["cifs_config"]["options"], list):
utils.throw_error("Expected [cifs_config.options] to be a list for [cifs] type")
disallowed_opts = ["user", "password"]
for opt in data["cifs_config"]["options"]:
if not isinstance(opt, str):
utils.throw_error("Expected [cifs_config.options] to be a list of strings for [cifs] type")
key = opt.split("=")[0]
for disallowed in disallowed_opts:
if key == disallowed:
utils.throw_error(f"Expected [cifs_config.options] to not start with [{disallowed}] for [cifs] type")
opts.append(opt)
server = data["cifs_config"]["server"].lstrip("/")
path = data["cifs_config"]["path"]
volume = {
"driver_opts": {
"type": "cifs",
"device": f"//{server}/{path}",
"o": f"{','.join(opts)}",
},
}
return volume
# Constructs a volume object for a nfs type
def _process_nfs(data):
if not data.get("nfs_config"):
utils.throw_error("Expected [nfs_config] to be set for [nfs] type")
required_keys = ["server", "path"]
for key in required_keys:
if not data["nfs_config"].get(key):
utils.throw_error(f"Expected [{key}] to be set for [nfs] type")
opts = [f"addr={data['nfs_config']['server']}"]
if data["nfs_config"].get("options"):
if not isinstance(data["nfs_config"]["options"], list):
utils.throw_error("Expected [nfs_config.options] to be a list for [nfs] type")
disallowed_opts = ["addr"]
for opt in data["nfs_config"]["options"]:
if not isinstance(opt, str):
utils.throw_error("Expected [nfs_config.options] to be a list of strings for [nfs] type")
key = opt.split("=")[0]
for disallowed in disallowed_opts:
if key == disallowed:
utils.throw_error(f"Expected [nfs_config.options] to not start with [{disallowed}] for [nfs] type")
opts.append(opt)
volume = {
"driver_opts": {
"type": "nfs",
"device": f":{data['nfs_config']['path']}",
"o": f"{','.join(opts)}",
},
}
return volume

View File

@@ -0,0 +1,18 @@
import secrets
import sys
class TemplateException(Exception):
pass
def throw_error(message):
# When throwing a known error, hide the traceback
# This is because the error is also shown in the UI
# and having a traceback makes it hard for user to read
sys.tracebacklimit = 0
raise TemplateException(message)
def secure_string(length):
return secrets.token_urlsafe(length)

View File

@@ -0,0 +1,48 @@
{% from "macros/global/perms/script.sh.jinja" import process_dir_func %}
{# Takes a list of items to process #}
{# Each item is a dictionary with the following keys: #}
{# - dir: directory to process #}
{# - mode: always, check. (
always: Always changes ownership and permissions,
check: Checks the top level dir, and only applies if there is a mismatch.
) #}
{# - uid: uid to change to #}
{# - gid: gid to change to #}
{# - chmod: chmod to change to (Optional, default is no change) #}
{% macro perms_container(items=[]) %}
image: bash
user: root
deploy:
resources:
limits:
cpus: "1.0"
memory: 512m
entrypoint:
- bash
- -c
command:
- |
{{- process_dir_func() | indent(4) }}
{%- for item in items %}
process_dir {{ item.dir }} {{ item.mode }} {{ item.uid }} {{ item.gid }} {{ item.chmod }}
{%- endfor %}
{% endmacro %}
{# Examples #}
{# perms_container([
{
"dir": "/mnt/directories/dir1",
"mode": "always",
"uid": 500,
"gid": 500,
"chmod": "755",
},
{
"dir": "/mnt/directories/dir2",
"mode": "check",
"uid": 500,
"gid": 500,
"chmod": "755",
},
]) #}

View File

@@ -0,0 +1,66 @@
{#
Don't forget to use double $ for shell variables,
otherwise docker-compose will try to expand them
#}
{% macro process_dir_func() %}
function process_dir() {
local dir=$$1
local mode=$$2
local uid=$$3
local gid=$$4
local chmod=$$5
local fix_owner="false"
local fix_perms="false"
if [ ! -d "$$dir" ]; then
echo "Path [$$dir] does is not a directory, skipping..."
exit 0
fi
echo "Current Ownership and Permissions on [$$dir]:"
echo "chown: $$(stat -c "%u %g" "$$dir")"
echo "chmod: $$(stat -c "%a" "$$dir")"
if [ "$$mode" = "always" ]; then
fix_owner="true"
fix_perms="true"
fi
if [ "$$mode" = "check" ]; then
if [ $$(stat -c %u "$$dir") -eq $$uid ] && [ $$(stat -c %g "$$dir") -eq $$gid ]; then
echo "Ownership is correct. Skipping..."
fix_owner="false"
else
echo "Ownership is incorrect. Fixing..."
fix_owner="true"
fi
if [ -n "$$chmod" ]; then
if [ $$(stat -c %a "$$dir") -eq $$chmod ]; then
echo "Permissions are correct. Skipping..."
fix_perms="false"
else
echo "Permissions are incorrect. Fixing..."
fix_perms="true"
fi
fi
fi
if [ "$$fix_owner" = "true" ]; then
echo "Changing ownership to $$uid:$$gid on: [$$dir]"
chown -R "$$uid:$$gid" "$$dir"
echo "Finished changing ownership"
echo "Ownership after changes:"
stat -c "%u %g" "$$dir"
fi
if [ -n "$$chmod" ] && [ "$$fix_perms" = "true" ]; then
echo "Changing permissions to $$chmod on: [$$dir]"
chmod -R "$$chmod" "$$dir"
echo "Finished changing permissions"
echo "Permissions after changes:"
stat -c "%a" "$$dir"
fi
}
{% endmacro %}