1
0
Fork 0

Compare commits

..

41 commits
v0.0.4 ... main

Author SHA1 Message Date
717d6a63b3
generate: generate: Bump to 0.0.15
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-13 17:52:55 +02:00
b9a8b87e7f
generate: container: Fix the image sanity check added by the last version
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-13 17:52:08 +02:00
8e4deb7d36
generate: generate: Bump to 0.0.14
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-12 18:34:15 +02:00
a8d0148f72
generate: container: Add or improve log messages
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-12 18:33:44 +02:00
589a9125f4
containerctl: Fix python version detection
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-12 18:16:07 +02:00
259c2ec8d7
generate: generate: Only log the file name when erroring out
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 20:14:14 +02:00
d5ea5e64ee
generate: generate: Bump version to 0.0.13
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:52:42 +02:00
9805419555
containerctl: Silence tee error messages
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:52:13 +02:00
b3fe3b8a10
containerctl: Add usage message
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:51:50 +02:00
94fcd6828f
containerctl: Expand python version check
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:47:52 +02:00
d67137363f
containerctl: Fix python version check
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:39:09 +02:00
2e793fd31f
generate: generate: Set the logging prefix after loading the json file
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:38:30 +02:00
11d6b574f3
generate: log: Add a log prefix
Add a log prefix. This enables logging the config file that contained
the error or warning, which is helpful when regenerating all containers.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-11 17:37:32 +02:00
e3125ea4fe
generate: generate: Bump to 0.0.12
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-09 20:19:35 +02:00
8f156e9f70
generate: container: Combine Network, Dns and Ports to ContainerNetwork
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-09 20:19:30 +02:00
aa13b77758
generate: generate: Bump version to 0.0.11
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 16:20:55 +02:00
147e5630aa
generate: container: Create new ContainerOptions class for read_only and Co
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 16:20:12 +02:00
e4ec47401e
generate: generate: Bump version to 0.0.10
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 16:01:37 +02:00
45d2e3a3d1
containerrc: Update JSON schema
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 16:00:52 +02:00
7a794197f4
generate: container: Allow one volume to be mounted multiple times
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 15:58:53 +02:00
8b7221363c
generate: container: Allow one secret to be used multiple times
Secret a can now be used more than once, but currently only with the same
secret type.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 15:51:53 +02:00
486a38440f
generate: container: Add command section to image configuration
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-08 15:43:39 +02:00
fb6aadb63c
generate: generate: Bump to 0.0.9
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 21:04:14 +02:00
2476d72192
generate: container: Update Network.from_json and Environment.command
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 21:03:33 +02:00
ec61600b87
generate: container: s/container crate/container create
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 21:00:39 +02:00
62d56248df
containerctl: Check if python3 is at least 3.11
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 20:37:28 +02:00
e4051fe4e4
generate: generate: Bump to 0.0.8
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 20:14:02 +02:00
cb2db03e4a
make: Update DESTDIR usage
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 20:13:34 +02:00
75b9b15f47
generate: container: Accept empty env section
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-07 20:12:00 +02:00
376fe7e5af
generate: generate: Bump to 0.0.7
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:43:11 +02:00
c68b1c288d
make: Create ENVDIR during install
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:42:39 +02:00
b638686cc4
generate: container: Change environment-file location
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:42:18 +02:00
6183b0907c
generate: generate: Add newline when adding placeholder content to script
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:39:48 +02:00
34b0d6f4ea
make: Don't add a / between PREFIX and the compound dir
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:35:48 +02:00
31d94b5266
generate: generate: Bump to 0.0.6
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:34:47 +02:00
260a0dcc52
generate: container: Improve optional feature handling
Things missing should now cause less problems.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 21:33:41 +02:00
6d9f63abb5
generate: container: Remove Nones and log_error() on missing optional keys
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 20:45:52 +02:00
4d86b49ed4
containerctl: s/generate_config/generate_container
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 20:38:22 +02:00
7a863bb7ea
containerctl: list_configs() should not use -type d
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 20:36:29 +02:00
7471bfbf70
generate: generate: Bump version to 0.0.5
Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 20:32:43 +02:00
76b8728fec
generate: container: Fix Ports.command
The long option is --publish, not --ports...

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-08-04 20:32:04 +02:00
6 changed files with 318 additions and 160 deletions

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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()}"

View file

@ -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)

View file

@ -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