Compare commits
41 commits
Author | SHA1 | Date | |
---|---|---|---|
717d6a63b3 | |||
b9a8b87e7f | |||
8e4deb7d36 | |||
a8d0148f72 | |||
589a9125f4 | |||
259c2ec8d7 | |||
d5ea5e64ee | |||
9805419555 | |||
b3fe3b8a10 | |||
94fcd6828f | |||
d67137363f | |||
2e793fd31f | |||
11d6b574f3 | |||
e3125ea4fe | |||
8f156e9f70 | |||
aa13b77758 | |||
147e5630aa | |||
e4ec47401e | |||
45d2e3a3d1 | |||
7a794197f4 | |||
8b7221363c | |||
486a38440f | |||
fb6aadb63c | |||
2476d72192 | |||
ec61600b87 | |||
62d56248df | |||
e4051fe4e4 | |||
cb2db03e4a | |||
75b9b15f47 | |||
376fe7e5af | |||
c68b1c288d | |||
b638686cc4 | |||
6183b0907c | |||
34b0d6f4ea | |||
31d94b5266 | |||
260a0dcc52 | |||
6d9f63abb5 | |||
4d86b49ed4 | |||
7a863bb7ea | |||
7471bfbf70 | |||
76b8728fec |
6 changed files with 318 additions and 160 deletions
19
Makefile
19
Makefile
|
@ -1,18 +1,19 @@
|
||||||
DESTDIR ?= /usr
|
BINDIR ?= $(DESTDIR)/usr/bin
|
||||||
BINDIR ?= $(DESTDIR)/bin
|
VARDIR ?= $(DESTDIR)/var/lib/containerctl
|
||||||
VARDIR ?= /var/lib/containerctl
|
|
||||||
GENERATEDIR ?= $(VARDIR)/generate
|
GENERATEDIR ?= $(VARDIR)/generate
|
||||||
CONFIGDIR ?= $(VARDIR)/configs
|
CONFIGDIR ?= $(VARDIR)/configs
|
||||||
CONTAINERDIR ?= $(VARDIR)/containers
|
CONTAINERDIR ?= $(VARDIR)/containers
|
||||||
|
ENVDIR ?= $(VARDIR)/environment-files
|
||||||
|
|
||||||
install:
|
install:
|
||||||
mkdir -p \
|
mkdir -p \
|
||||||
$(PREFIX)/$(GENERATEDIR) \
|
$(PREFIX)$(GENERATEDIR) \
|
||||||
$(PREFIX)/$(CONFIGDIR) \
|
$(PREFIX)$(CONFIGDIR) \
|
||||||
$(PREFIX)/$(CONTAINERDIR) \
|
$(PREFIX)$(CONTAINERDIR) \
|
||||||
$(PREFIX)/$(BINDIR)
|
$(PREFIX)$(ENVDIR) \
|
||||||
install -m755 containerctl $(PREFIX)/$(BINDIR)
|
$(PREFIX)$(BINDIR)
|
||||||
cp -t $(PREFIX)/$(GENERATEDIR) \
|
install -m755 containerctl $(PREFIX)$(BINDIR)
|
||||||
|
cp -t $(PREFIX)$(GENERATEDIR) \
|
||||||
generate/container.py \
|
generate/container.py \
|
||||||
generate/log.py \
|
generate/log.py \
|
||||||
generate/generate.py
|
generate/generate.py
|
||||||
|
|
48
containerctl
48
containerctl
|
@ -11,24 +11,46 @@ LOGDIR="/var/log/containerctl"
|
||||||
TODAY="$(date '+%F')"
|
TODAY="$(date '+%F')"
|
||||||
LOG="${LOGDIR}/${TODAY}"
|
LOG="${LOGDIR}/${TODAY}"
|
||||||
|
|
||||||
log_error()
|
get_python_path()
|
||||||
{
|
{
|
||||||
printf '[%b] (EE) %b\n' "${TODAY}" "${@}" | tee -a "${LOG}"
|
py="python3"
|
||||||
|
pyver="$(/usr/bin/env "${py}" -c 'import sys; print(sys.version_info.minor)')"
|
||||||
|
if [ "${pyver}" -lt "11" ]
|
||||||
|
then
|
||||||
|
pyver="13"
|
||||||
|
py="python3.${pyver}"
|
||||||
|
else
|
||||||
|
printf '%b' "${py}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ "${pyver}" -ge 11 ]
|
||||||
|
do
|
||||||
|
if /usr/bin/env "${py}" -c "print('${py}')" 2> /dev/null
|
||||||
|
then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
pyver=$((pyver - 1))
|
||||||
|
py="python3.${pyver}"
|
||||||
|
done
|
||||||
|
|
||||||
|
log_error 'containerctl needs at least Python 3.11 to run!'
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
list_dir()
|
log_error()
|
||||||
{
|
{
|
||||||
find "${1}" -mindepth 1 -type d
|
printf '[%b] (EE) %b\n' "${TODAY}" "${@}" | tee -a "${LOG}" 2> /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
list_containers()
|
list_containers()
|
||||||
{
|
{
|
||||||
list_dir "${CONTAINERDIR}"
|
find "${CONTAINERDIR}" -mindepth 1 -type d
|
||||||
}
|
}
|
||||||
|
|
||||||
list_configs()
|
list_configs()
|
||||||
{
|
{
|
||||||
list_dir "${CONFIGDIR}"
|
find "${CONFIGDIR}" -mindepth 1 -type f
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_script()
|
exec_script()
|
||||||
|
@ -111,7 +133,8 @@ generate_container()
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/usr/bin/env python3 "${BASEDIR}/generate/generate.py" \
|
mypython="$(get_python_path)"
|
||||||
|
/usr/bin/env "${mypython}" "${BASEDIR}/generate/generate.py" \
|
||||||
"${CONFIGDIR}/${config}" "${LOG}" "${CONTAINERDIR}"
|
"${CONFIGDIR}/${config}" "${LOG}" "${CONTAINERDIR}"
|
||||||
|
|
||||||
if [ "${?}" = 1 ]
|
if [ "${?}" = 1 ]
|
||||||
|
@ -125,14 +148,21 @@ generate_all()
|
||||||
{
|
{
|
||||||
list_configs | while read -r config
|
list_configs | while read -r config
|
||||||
do
|
do
|
||||||
generate_config "$(printf '%b' "${config}" | sed -e "s|${CONFIGDIR}||g")"
|
generate_container "$(printf '%b' "${config}" | sed -e "s|${CONFIGDIR}||g")"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage()
|
||||||
|
{
|
||||||
|
printf '%b list-containers|list-configs|generate CONFIG-FILE|generate-all|CONTAINER-NAME ACTION\n' "${0}"
|
||||||
|
}
|
||||||
|
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
"list-containers") list_containers ;;
|
"list-containers") list_containers ;;
|
||||||
"list-configs") list_configs ;;
|
"list-configs") list_configs ;;
|
||||||
"generate-all") generate_all ;;
|
"generate-all") generate_all ;;
|
||||||
"generate") shift; generate_config "${@}" ;;
|
"generate") shift; generate_container "${@}" ;;
|
||||||
|
"help") usage "${0}" ;;
|
||||||
|
"usage") usage "${0}" ;;
|
||||||
*) exec_script "${@}" ;;
|
*) exec_script "${@}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
},
|
},
|
||||||
"tag": {
|
"tag": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -52,11 +55,7 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {}
|
"items": {}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"required": [
|
|
||||||
"mode",
|
|
||||||
"options"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -72,11 +71,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"required": [
|
|
||||||
"search",
|
|
||||||
"servers"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ports": {
|
"ports": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -97,11 +92,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"required": [
|
|
||||||
"tcp",
|
|
||||||
"udp"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -132,11 +123,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"required": [
|
|
||||||
"add",
|
|
||||||
"drop"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"accounting": {
|
"accounting": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
@ -186,6 +186,7 @@ class Memory:
|
||||||
reservation = maybe_or(val, "reservation", "")
|
reservation = maybe_or(val, "reservation", "")
|
||||||
swap = maybe_or(val, "swap", "")
|
swap = maybe_or(val, "swap", "")
|
||||||
if limit == "":
|
if limit == "":
|
||||||
|
logger.log_warning("No limit set, memory config is not needed")
|
||||||
return cls("", "", "")
|
return cls("", "", "")
|
||||||
return cls(limit, reservation, swap)
|
return cls(limit, reservation, swap)
|
||||||
|
|
||||||
|
@ -244,17 +245,26 @@ class Volume:
|
||||||
"""Container Volume."""
|
"""Container Volume."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
path: str
|
path: list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
|
def from_json(cls, val: ConfigValue, logger: Log) -> list:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
if val is None:
|
if val is None:
|
||||||
return None
|
return []
|
||||||
if not isinstance(val, dict):
|
if not isinstance(val, dict):
|
||||||
logger.log_warning("Volume key is present, but malformed.")
|
logger.log_warning("Volume key is malformed.")
|
||||||
return None
|
return []
|
||||||
return [cls(key, value) for key, value in val.items()]
|
return [
|
||||||
|
Volume.from_json_entry(key, value) for key, value in val.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json_entry(cls, key: str, value: str | list) -> Self:
|
||||||
|
"""Create from JSON entry."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
return cls(key, [value])
|
||||||
|
return cls(key, value)
|
||||||
|
|
||||||
def is_host_volume(self) -> bool:
|
def is_host_volume(self) -> bool:
|
||||||
"""Check if this Volume is a named or a host volume."""
|
"""Check if this Volume is a named or a host volume."""
|
||||||
|
@ -262,7 +272,10 @@ class Volume:
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
return f"--volume {self.name}:{self.path}"
|
cmd = ""
|
||||||
|
for path in self.path:
|
||||||
|
cmd += f"\t--volume {self.name}:{path} \\\n"
|
||||||
|
return cmd
|
||||||
|
|
||||||
def create(self) -> str:
|
def create(self) -> str:
|
||||||
"""Create volume, if it does not exist."""
|
"""Create volume, if it does not exist."""
|
||||||
|
@ -298,17 +311,17 @@ class Secret:
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
secret_type: str
|
secret_type: str
|
||||||
target: str
|
target: list
|
||||||
options: str
|
options: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
|
def from_json(cls, val: ConfigValue, logger: Log) -> list:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
if val is None:
|
if val is None:
|
||||||
return None
|
return []
|
||||||
if not isinstance(val, dict):
|
if not isinstance(val, dict):
|
||||||
logger.log_warning("Secret key is present, but malformed!")
|
logger.log_warning("Secret key is malformed!")
|
||||||
return None
|
return []
|
||||||
secrets = []
|
secrets = []
|
||||||
for key in val:
|
for key in val:
|
||||||
if not isinstance(val[key], dict):
|
if not isinstance(val[key], dict):
|
||||||
|
@ -316,26 +329,36 @@ class Secret:
|
||||||
continue
|
continue
|
||||||
name = key
|
name = key
|
||||||
secret_type = maybe_or(val[key], "type", "")
|
secret_type = maybe_or(val[key], "type", "")
|
||||||
target = maybe_or(val[key], "target", "")
|
target = maybe(val[key], "target")
|
||||||
|
if isinstance(target, str):
|
||||||
|
target = [target]
|
||||||
|
if not isinstance(target, list):
|
||||||
|
logger.log_warning(
|
||||||
|
f"Secret {name} has no target and will be ignored"
|
||||||
|
)
|
||||||
|
target = []
|
||||||
options = maybe_or(val[key], "options", "")
|
options = maybe_or(val[key], "options", "")
|
||||||
if options is None:
|
if options is None:
|
||||||
options = ""
|
options = ""
|
||||||
secrets.append(
|
secrets.append(cls(name, str(secret_type), target, str(options)))
|
||||||
cls(name, str(secret_type), str(target), str(options))
|
|
||||||
)
|
|
||||||
|
|
||||||
return secrets
|
return secrets
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
cmd = (
|
cmd = ""
|
||||||
f"--secret {self.name},:"
|
for target in self.target:
|
||||||
|
cmd += (
|
||||||
|
f"\t--secret {self.name},:"
|
||||||
f"type={self.secret_type},"
|
f"type={self.secret_type},"
|
||||||
f"target={self.target}"
|
f"target={target}"
|
||||||
)
|
)
|
||||||
# Not a password, ruff...
|
# Not a password, ruff...
|
||||||
if self.secret_type == "mount" and self.options != "": # noqa: S105
|
has_option = self.secret_type == "mount" # noqa: S105
|
||||||
|
has_option = has_option and self.options != ""
|
||||||
|
if has_option:
|
||||||
cmd = f"{cmd},{self.options}"
|
cmd = f"{cmd},{self.options}"
|
||||||
|
cmd += " \\\n"
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
@ -374,22 +397,29 @@ class Environment:
|
||||||
if val is None:
|
if val is None:
|
||||||
return cls([], "")
|
return cls([], "")
|
||||||
if not isinstance(val, dict):
|
if not isinstance(val, dict):
|
||||||
logger.log_warning("Environment key is present, but malformed!")
|
logger.log_warning("Environment key is malformed!")
|
||||||
return cls([], "")
|
return cls([], "")
|
||||||
return cls([f"{key}='{value}'" for key, value in val.items()], "")
|
return cls([f"{key}='{value}'" for key, value in val.items()], "")
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
return f"--env-file={self.file}"
|
if len(self.variables) == 0:
|
||||||
|
return ""
|
||||||
|
return f"\t--env-file={self.file} \\\n"
|
||||||
|
|
||||||
def create(self) -> str:
|
def create(self) -> str:
|
||||||
"""Create env file."""
|
"""Create env file."""
|
||||||
cmd = f"# Create env-file {self.file}\n"
|
cmd = ""
|
||||||
|
header = f"# Create env-file {self.file}\n"
|
||||||
|
|
||||||
for var in self.variables:
|
for var in self.variables:
|
||||||
escaped_var = var.replace("'", "%b")
|
escaped_var = var.replace("'", "%b")
|
||||||
cmd += f"printf '{escaped_var}\\n' \"'\" \"'\" >> '{self.file}'\n"
|
cmd += f"printf '{escaped_var}\\n' \"'\" \"'\" >> '{self.file}'\n"
|
||||||
|
|
||||||
return cmd
|
if cmd == "":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return header + f"printf '\\n' > {self.file}\n" + cmd
|
||||||
|
|
||||||
def remove(self) -> str:
|
def remove(self) -> str:
|
||||||
"""Remove env file."""
|
"""Remove env file."""
|
||||||
|
@ -414,31 +444,45 @@ class Ports:
|
||||||
if val is None:
|
if val is None:
|
||||||
return cls([], [])
|
return cls([], [])
|
||||||
if not isinstance(val, dict):
|
if not isinstance(val, dict):
|
||||||
logger.log_warning("Ports key is present, but malformed!")
|
logger.log_warning("Ports key is malformed!")
|
||||||
return cls([], [])
|
return cls([], [])
|
||||||
tcp_ports = maybe(val, "tcp")
|
tcp_ports = maybe(val, "tcp")
|
||||||
udp_ports = maybe(val, "udp")
|
udp_ports = maybe(val, "udp")
|
||||||
|
if tcp_ports is None:
|
||||||
|
tcp_ports = []
|
||||||
|
if udp_ports is None:
|
||||||
|
udp_ports = []
|
||||||
|
if not isinstance(tcp_ports, list) and not isinstance(udp_ports, list):
|
||||||
|
logger.log_warning("Port configuration is malformed!")
|
||||||
|
return cls([], [])
|
||||||
if not isinstance(tcp_ports, list):
|
if not isinstance(tcp_ports, list):
|
||||||
logger.log_warning("Key tcp_ports is not an array!")
|
logger.log_warning("tcp_ports configuration is malformed!")
|
||||||
return cls([], [])
|
return cls([], udp_ports)
|
||||||
if not isinstance(udp_ports, list):
|
if not isinstance(udp_ports, list):
|
||||||
logger.log_warning("Key udp_ports is not an array!")
|
logger.log_warning("udp_ports configuration is malformed!")
|
||||||
return cls([], [])
|
return cls(tcp_ports, [])
|
||||||
return cls(tcp_ports, udp_ports)
|
return cls(tcp_ports, udp_ports)
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
ports = ""
|
|
||||||
seperator = " \\\n"
|
seperator = " \\\n"
|
||||||
ports += seperator.join(
|
tcp_ports = seperator.join(
|
||||||
[f"\t--port {port}/tcp" for port in self.tcp_ports]
|
[f"\t--publish {port}/tcp" for port in self.tcp_ports]
|
||||||
)
|
)
|
||||||
ports += seperator
|
udp_ports = seperator.join(
|
||||||
ports += seperator.join(
|
[f"\t--publish {port}/udp" for port in self.udp_ports]
|
||||||
[f"\t--port {port}/udp" for port in self.udp_ports]
|
|
||||||
)
|
)
|
||||||
ports += seperator
|
|
||||||
return ports
|
if tcp_ports == "" and udp_ports == "":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if tcp_ports == "":
|
||||||
|
return udp_ports + seperator
|
||||||
|
|
||||||
|
if udp_ports == "":
|
||||||
|
return tcp_ports + seperator
|
||||||
|
|
||||||
|
return tcp_ports + seperator + udp_ports + seperator
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -451,26 +495,31 @@ class Network:
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
if val is None or not isinstance(val, dict):
|
if val is None:
|
||||||
logger.log_error("Network configuration is missing or malformed!")
|
return cls("", [])
|
||||||
|
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_warning("Network configuration is malformed!")
|
||||||
return cls("", [])
|
return cls("", [])
|
||||||
mode = maybe(val, "mode")
|
mode = maybe(val, "mode")
|
||||||
options = maybe(val, "options")
|
options = maybe(val, "options")
|
||||||
if mode is None or options is None or not isinstance(options, list):
|
if mode is None or not isinstance(mode, str):
|
||||||
err = "Network configuration is missing or has malformed elements!"
|
err = "Network configuration is missing or has malformed elements!"
|
||||||
logger.log_error(err)
|
logger.log_error(err)
|
||||||
return cls("", [])
|
return cls("", [])
|
||||||
return cls(str(mode), options)
|
if options is None or not isinstance(options, list):
|
||||||
|
return cls(mode, [])
|
||||||
|
return cls(mode, options)
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
if self.mode == "":
|
if self.mode == "":
|
||||||
return ""
|
return ""
|
||||||
cmd = f"--network={self.mode}"
|
cmd = f"\t--network={self.mode}"
|
||||||
opts = ",".join(self.options)
|
opts = ",".join(self.options)
|
||||||
if opts != "":
|
if opts != "":
|
||||||
cmd += f":{opts}"
|
cmd += f":{opts}"
|
||||||
return cmd
|
return cmd + " \\\n"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -480,6 +529,7 @@ class Image:
|
||||||
registry: str
|
registry: str
|
||||||
image: str
|
image: str
|
||||||
tag: str
|
tag: str
|
||||||
|
cmd: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> Self | None:
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self | None:
|
||||||
|
@ -490,10 +540,13 @@ class Image:
|
||||||
registry = maybe_or(val, "registry", "")
|
registry = maybe_or(val, "registry", "")
|
||||||
image = maybe_or(val, "image", "")
|
image = maybe_or(val, "image", "")
|
||||||
tag = maybe_or(val, "tag", "")
|
tag = maybe_or(val, "tag", "")
|
||||||
return cls(str(registry), str(image), str(tag))
|
cmd = maybe_or(val, "command", "")
|
||||||
|
return cls(registry, image, tag, cmd)
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
|
if self.cmd != "":
|
||||||
|
return f"{self.registry}/{self.image}:{self.tag} {self.cmd}"
|
||||||
return f"{self.registry}/{self.image}:{self.tag}"
|
return f"{self.registry}/{self.image}:{self.tag}"
|
||||||
|
|
||||||
|
|
||||||
|
@ -505,13 +558,13 @@ class Capability:
|
||||||
mode: str
|
mode: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
|
def from_json(cls, val: ConfigValue, logger: Log) -> list:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
if val is None or not isinstance(val, dict):
|
if val is None:
|
||||||
logger.log_warning(
|
return []
|
||||||
"Capabilities key is either missing or malformed!"
|
if not isinstance(val, dict):
|
||||||
)
|
logger.log_warning("Capabilities key is malformed!")
|
||||||
return None
|
return []
|
||||||
add = [cls(value, "add") for value in val["add"]]
|
add = [cls(value, "add") for value in val["add"]]
|
||||||
drop = [cls(value, "drop") for value in val["drop"]]
|
drop = [cls(value, "drop") for value in val["drop"]]
|
||||||
return add + drop
|
return add + drop
|
||||||
|
@ -531,49 +584,133 @@ class Dns:
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
if val is None or not isinstance(val, dict):
|
if val is None:
|
||||||
logger.log_error("DNS Key is either missing or malformed!")
|
return cls([], "")
|
||||||
|
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_warning("DNS Key is malformed!")
|
||||||
return cls([], "")
|
return cls([], "")
|
||||||
search = maybe_or(val, "search", "")
|
search = maybe_or(val, "search", "")
|
||||||
servers = maybe(val, "servers")
|
servers = maybe(val, "servers")
|
||||||
if not isinstance(servers, list):
|
if not isinstance(servers, list):
|
||||||
logger.log_error("Servers key is not an array!")
|
logger.log_warning("Servers key is not an array!")
|
||||||
return cls([], "")
|
return cls([], search)
|
||||||
return cls(servers, str(search))
|
return cls(servers, search)
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
"""Option for podman container create."""
|
"""Option for podman container create."""
|
||||||
if len(self.servers) == 0 and self.search == "":
|
if len(self.servers) == 0 and self.search == "":
|
||||||
return ""
|
return ""
|
||||||
if len(self.servers) == 0:
|
if len(self.servers) == 0:
|
||||||
return f"--dns-search={self.search}"
|
return f"\t--dns-search={self.search} \\\n"
|
||||||
if self.search == "":
|
if self.search == "":
|
||||||
return f"--dns={','.join(self.servers)}"
|
return f"\t--dns={','.join(self.servers)} \\\n"
|
||||||
|
|
||||||
cmd = f"--dns-search={self.search} \\\n\t--dns="
|
cmd = f"\t--dns-search={self.search} \\\n\t--dns="
|
||||||
cmd += ",".join(self.servers)
|
cmd += ",".join(self.servers)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContainerOptions:
|
||||||
|
"""Container-Meta settings."""
|
||||||
|
|
||||||
|
privileged: bool = False
|
||||||
|
read_only: bool = False
|
||||||
|
replace: bool = False
|
||||||
|
restart: str = "no"
|
||||||
|
pull_policy: str = "always"
|
||||||
|
timezone: str = "local"
|
||||||
|
is_valid: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
if val is None:
|
||||||
|
# Should not happen!
|
||||||
|
return cls()
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_error("Container config is invalid!")
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
privileged = maybe_or(val, "privileged", _or=False)
|
||||||
|
read_only = maybe_or(val, "read_only", _or=False)
|
||||||
|
replace = maybe_or(val, "replace", _or=False)
|
||||||
|
restart = maybe_or(val, "restart", "no")
|
||||||
|
pull_policy = maybe_or(val, "pull_policy", "always")
|
||||||
|
timezone = maybe_or(val, "timezone", "local")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
privileged,
|
||||||
|
read_only,
|
||||||
|
replace,
|
||||||
|
restart,
|
||||||
|
pull_policy,
|
||||||
|
timezone,
|
||||||
|
is_valid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Option for podman conainter create."""
|
||||||
|
cmd = ""
|
||||||
|
if self.privileged:
|
||||||
|
cmd += "\t--privileged \\\n"
|
||||||
|
if self.read_only:
|
||||||
|
cmd += "\t--read-only \\\n"
|
||||||
|
if self.replace:
|
||||||
|
cmd += "\t--replace \\\n"
|
||||||
|
if self.restart != "":
|
||||||
|
cmd += f"\t--restart={self.restart} \\\n"
|
||||||
|
if self.pull_policy != "":
|
||||||
|
cmd += f"\t--pull-policy={self.pull_policy} \\\n"
|
||||||
|
if self.timezone != "":
|
||||||
|
cmd += f"\t--tz={self.timezone} \\\n"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContainerNetwork:
|
||||||
|
"""Wrapper for Network, Dns and Ports."""
|
||||||
|
|
||||||
|
network: Network
|
||||||
|
dns: Dns
|
||||||
|
ports: Ports
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, json: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
network_config = maybe(json, "network")
|
||||||
|
dns_config = maybe(json, "dns")
|
||||||
|
ports_config = maybe(json, "ports")
|
||||||
|
|
||||||
|
network = Network.from_json(network_config, logger)
|
||||||
|
dns = Dns.from_json(dns_config, logger)
|
||||||
|
ports = Ports.from_json(ports_config, logger)
|
||||||
|
|
||||||
|
return cls(network, dns, ports)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Option for podman container create."""
|
||||||
|
cmd = ""
|
||||||
|
cmd += self.network.command()
|
||||||
|
cmd += self.dns.command()
|
||||||
|
cmd += self.ports.command()
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
class Container:
|
class Container:
|
||||||
"""Container."""
|
"""Container."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
image: Image
|
image: Image
|
||||||
privileged: bool
|
ct_opts: ContainerOptions
|
||||||
read_only: bool
|
ct_network: ContainerNetwork
|
||||||
replace: bool
|
env: Environment
|
||||||
restart: str
|
secrets: list
|
||||||
pull_policy: str
|
volumes: list
|
||||||
timezone: str
|
capabilities: list
|
||||||
network: Network
|
|
||||||
dns: Dns
|
|
||||||
ports: Ports
|
|
||||||
env: Environment | None
|
|
||||||
secrets: list | None
|
|
||||||
volumes: list | None
|
|
||||||
capabilities: list | None
|
|
||||||
accounting: Accounting
|
accounting: Accounting
|
||||||
|
|
||||||
def __init__(self, json: dict, logger: Log | None = None) -> None:
|
def __init__(self, json: dict, logger: Log | None = None) -> None:
|
||||||
|
@ -583,20 +720,30 @@ class Container:
|
||||||
name = maybe(json, "name")
|
name = maybe(json, "name")
|
||||||
if name is None:
|
if name is None:
|
||||||
logger.log_error("No container name set, aborting!")
|
logger.log_error("No container name set, aborting!")
|
||||||
return
|
raise ConfigError("Container has no name")
|
||||||
image = maybe(json, "image")
|
image = maybe(json, "image")
|
||||||
if image is None:
|
if image is None:
|
||||||
logger.log_error("No image set, aborting!")
|
logger.log_error("No image set, aborting!")
|
||||||
return
|
raise ConfigError("Container has no image")
|
||||||
privileged = maybe(json, "privileged")
|
|
||||||
read_only = maybe(json, "read_only")
|
self.image = Image.from_json(image, logger)
|
||||||
replace = maybe(json, "replace")
|
image_valid = True
|
||||||
pull_policy = maybe_or(json, "pull_policy", "always")
|
if self.image.image == "":
|
||||||
restart = maybe_or(json, "restart", "no")
|
logger.log_error("Image has no image set!")
|
||||||
timezone = maybe_or(json, "timezone", "local")
|
image_valid = False
|
||||||
network = maybe(json, "network")
|
if self.image.registry == "":
|
||||||
dns = maybe(json, "dns")
|
logger.log_error("Image has no registry set!")
|
||||||
ports = maybe(json, "ports")
|
image_valid = False
|
||||||
|
if self.image.tag == "":
|
||||||
|
logger.log_error("Image has no tag set!")
|
||||||
|
image_valid = False
|
||||||
|
if not image_valid:
|
||||||
|
raise ConfigError("Image is missing required keys!")
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
ct_opts = ContainerOptions.from_json(json, logger)
|
||||||
|
if not ct_opts.is_valid:
|
||||||
|
raise ConfigError("Config seems to be invalid?")
|
||||||
env = maybe(json, "env")
|
env = maybe(json, "env")
|
||||||
secrets = maybe(json, "secrets")
|
secrets = maybe(json, "secrets")
|
||||||
volumes = maybe(json, "volumes")
|
volumes = maybe(json, "volumes")
|
||||||
|
@ -604,18 +751,11 @@ class Container:
|
||||||
accounting = maybe(json, "accounting")
|
accounting = maybe(json, "accounting")
|
||||||
self.name = str(name)
|
self.name = str(name)
|
||||||
self.image = Image.from_json(image, logger)
|
self.image = Image.from_json(image, logger)
|
||||||
self.privileged = privileged is not None and bool(privileged)
|
self.ct_opts = ct_opts
|
||||||
self.read_only = read_only is not None and bool(read_only)
|
self.ct_network = ContainerNetwork.from_json(json, logger)
|
||||||
self.replace = replace is not None and bool(replace)
|
|
||||||
self.pull_policy = str(pull_policy)
|
|
||||||
self.restart = str(restart)
|
|
||||||
self.timezone = str(timezone)
|
|
||||||
self.network = Network.from_json(network, logger)
|
|
||||||
self.dns = Dns.from_json(dns, logger)
|
|
||||||
self.ports = Ports.from_json(ports, logger)
|
|
||||||
self.env = Environment.from_json(env, logger)
|
self.env = Environment.from_json(env, logger)
|
||||||
self.env.file = "/var/lib/containerctl/containers/"
|
self.env.file = "/var/lib/containerctl/environment-files/"
|
||||||
self.env.file += f"{self.name}/environment"
|
self.env.file += f"{self.name}"
|
||||||
self.secrets = Secret.from_json(secrets, logger)
|
self.secrets = Secret.from_json(secrets, logger)
|
||||||
self.volumes = Volume.from_json(volumes, logger)
|
self.volumes = Volume.from_json(volumes, logger)
|
||||||
self.capabilities = Capability.from_json(capabilities, logger)
|
self.capabilities = Capability.from_json(capabilities, logger)
|
||||||
|
@ -648,26 +788,15 @@ class Container:
|
||||||
def create_container(self) -> str:
|
def create_container(self) -> str:
|
||||||
"""Generate podman container create command."""
|
"""Generate podman container create command."""
|
||||||
cmd = f"# Create container {self.name}\n"
|
cmd = f"# Create container {self.name}\n"
|
||||||
cmd += "podman container crate \\\n"
|
cmd += "podman container create \\\n"
|
||||||
cmd += f"\t--name={self.name} \\\n"
|
cmd += f"\t--name={self.name} \\\n"
|
||||||
if self.privileged:
|
cmd += f"{self.ct_opts.command()}"
|
||||||
cmd += "\t--privileged \\\n"
|
cmd += f"{self.ct_network.command()}"
|
||||||
if self.replace:
|
cmd += f"{self.env.command()}"
|
||||||
cmd += "\t--replace \\\n"
|
|
||||||
if self.read_only:
|
|
||||||
cmd += "\t--read-only \\\n"
|
|
||||||
cmd += f"\t--restart={self.restart} \\\n"
|
|
||||||
cmd += f"\t--pull={self.pull_policy} \\\n"
|
|
||||||
cmd += f"\t--tz={self.timezone} \\\n"
|
|
||||||
cmd += f"\t{self.network.command()} \\\n"
|
|
||||||
cmd += f"\t{self.dns.command()} \\\n"
|
|
||||||
cmd += f"{self.ports.command()}"
|
|
||||||
if self.env is not None:
|
|
||||||
cmd += f"\t{self.env.command()} \\\n"
|
|
||||||
for secret in self.secrets:
|
for secret in self.secrets:
|
||||||
cmd += f"\t{secret.command()} \\\n"
|
cmd += f"{secret.command()}"
|
||||||
for volume in self.volumes:
|
for volume in self.volumes:
|
||||||
cmd += f"\t{volume.command()} \\\n"
|
cmd += f"{volume.command()}"
|
||||||
for capability in self.capabilities:
|
for capability in self.capabilities:
|
||||||
cmd += f"\t{capability.command()} \\\n"
|
cmd += f"\t{capability.command()} \\\n"
|
||||||
cmd += f"{self.accounting.command()}"
|
cmd += f"{self.accounting.command()}"
|
||||||
|
|
|
@ -14,7 +14,7 @@ from pathlib import Path
|
||||||
from container import ConfigError, Container
|
from container import ConfigError, Container
|
||||||
from log import Log
|
from log import Log
|
||||||
|
|
||||||
GENERATE_VERSION = "0.0.4"
|
GENERATE_VERSION = "0.0.15"
|
||||||
HEADER = f"""#!/bin/sh
|
HEADER = f"""#!/bin/sh
|
||||||
# This script was generated by containerctl v{GENERATE_VERSION}
|
# This script was generated by containerctl v{GENERATE_VERSION}
|
||||||
# Report bugs with _this script_ to <tenno+containerctl@suij.in>
|
# Report bugs with _this script_ to <tenno+containerctl@suij.in>
|
||||||
|
@ -76,10 +76,12 @@ def main() -> None:
|
||||||
if len(sys.argv) > log_threshold:
|
if len(sys.argv) > log_threshold:
|
||||||
base = sys.argv[3]
|
base = sys.argv[3]
|
||||||
logger = Log(log_file)
|
logger = Log(log_file)
|
||||||
data = load_container_config(Path(config_file), logger)
|
conf = Path(config_file)
|
||||||
|
data = load_container_config(conf, logger)
|
||||||
if data is None:
|
if data is None:
|
||||||
logger.log_error(f"{config_file} is invalid, aborting!")
|
logger.log_error(f"{conf.name} is invalid, aborting!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
logger.set_prefix(conf.name)
|
||||||
ct = create_container_from_config(data, logger)
|
ct = create_container_from_config(data, logger)
|
||||||
if ct is None:
|
if ct is None:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -109,7 +111,7 @@ def main() -> None:
|
||||||
script_content += ct.create_environment()
|
script_content += ct.create_environment()
|
||||||
script_content += method()
|
script_content += method()
|
||||||
if script_content == "":
|
if script_content == "":
|
||||||
script_content = "true"
|
script_content = "true\n"
|
||||||
with s.open("w+", encoding="utf-8") as f:
|
with s.open("w+", encoding="utf-8") as f:
|
||||||
f.write(HEADER)
|
f.write(HEADER)
|
||||||
f.write(script_content)
|
f.write(script_content)
|
||||||
|
|
|
@ -15,6 +15,7 @@ class Log:
|
||||||
|
|
||||||
messages: list = []
|
messages: list = []
|
||||||
logfile: Path
|
logfile: Path
|
||||||
|
prefix: str = ""
|
||||||
|
|
||||||
def __init__(self, path: str) -> None:
|
def __init__(self, path: str) -> None:
|
||||||
"""Init for Log."""
|
"""Init for Log."""
|
||||||
|
@ -25,15 +26,19 @@ class Log:
|
||||||
def log_error(self, msg: str) -> None:
|
def log_error(self, msg: str) -> None:
|
||||||
"""Log an error."""
|
"""Log an error."""
|
||||||
now = self.timestamp()
|
now = self.timestamp()
|
||||||
prefix = "EE"
|
prefix = "(EE)"
|
||||||
log_message = f"[{now}] ({prefix}) {msg}"
|
if self.prefix != "":
|
||||||
|
prefix += f" {self.prefix}:"
|
||||||
|
log_message = f"[{now}] {prefix} {msg}"
|
||||||
self.write_message(log_message)
|
self.write_message(log_message)
|
||||||
|
|
||||||
def log_warning(self, msg: str) -> None:
|
def log_warning(self, msg: str) -> None:
|
||||||
"""Log a warning."""
|
"""Log a warning."""
|
||||||
now = self.timestamp()
|
now = self.timestamp()
|
||||||
prefix = "WW"
|
prefix = "(WW)"
|
||||||
log_message = f"[{now}] ({prefix}) {msg}"
|
if self.prefix != "":
|
||||||
|
prefix += f" {self.prefix}:"
|
||||||
|
log_message = f"[{now}] {prefix} {msg}"
|
||||||
self.write_message(log_message)
|
self.write_message(log_message)
|
||||||
|
|
||||||
def write_message(self, msg: str) -> None:
|
def write_message(self, msg: str) -> None:
|
||||||
|
@ -49,3 +54,7 @@ class Log:
|
||||||
return datetime.datetime.now(tz=datetime.UTC).strftime(
|
return datetime.datetime.now(tz=datetime.UTC).strftime(
|
||||||
"%Y-%m-%d %H:%M:%S",
|
"%Y-%m-%d %H:%M:%S",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_prefix(self, prefix: str) -> None:
|
||||||
|
"""Set a prefix."""
|
||||||
|
self.prefix = prefix
|
||||||
|
|
Loading…
Add table
Reference in a new issue