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)/bin
VARDIR ?= /var/lib/containerctl
BINDIR ?= $(DESTDIR)/usr/bin
VARDIR ?= $(DESTDIR)/var/lib/containerctl
GENERATEDIR ?= $(VARDIR)/generate
CONFIGDIR ?= $(VARDIR)/configs
CONTAINERDIR ?= $(VARDIR)/containers
ENVDIR ?= $(VARDIR)/environment-files
install:
mkdir -p \
$(PREFIX)/$(GENERATEDIR) \
$(PREFIX)/$(CONFIGDIR) \
$(PREFIX)/$(CONTAINERDIR) \
$(PREFIX)/$(BINDIR)
install -m755 containerctl $(PREFIX)/$(BINDIR)
cp -t $(PREFIX)/$(GENERATEDIR) \
$(PREFIX)$(GENERATEDIR) \
$(PREFIX)$(CONFIGDIR) \
$(PREFIX)$(CONTAINERDIR) \
$(PREFIX)$(ENVDIR) \
$(PREFIX)$(BINDIR)
install -m755 containerctl $(PREFIX)$(BINDIR)
cp -t $(PREFIX)$(GENERATEDIR) \
generate/container.py \
generate/log.py \
generate/generate.py

View file

@ -11,24 +11,46 @@ LOGDIR="/var/log/containerctl"
TODAY="$(date '+%F')"
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_dir "${CONTAINERDIR}"
find "${CONTAINERDIR}" -mindepth 1 -type d
}
list_configs()
{
list_dir "${CONFIGDIR}"
find "${CONFIGDIR}" -mindepth 1 -type f
}
exec_script()
@ -111,7 +133,8 @@ generate_container()
exit 1
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}"
if [ "${?}" = 1 ]
@ -125,14 +148,21 @@ generate_all()
{
list_configs | while read -r config
do
generate_config "$(printf '%b' "${config}" | sed -e "s|${CONFIGDIR}||g")"
generate_container "$(printf '%b' "${config}" | sed -e "s|${CONFIGDIR}||g")"
done
}
usage()
{
printf '%b list-containers|list-configs|generate CONFIG-FILE|generate-all|CONTAINER-NAME ACTION\n' "${0}"
}
case "${1}" in
"list-containers") list_containers ;;
"list-configs") list_configs ;;
"generate-all") generate_all ;;
"generate") shift; generate_config "${@}" ;;
"generate") shift; generate_container "${@}" ;;
"help") usage "${0}" ;;
"usage") usage "${0}" ;;
*) exec_script "${@}" ;;
esac

View file

@ -16,6 +16,9 @@
},
"tag": {
"type": "string"
},
"command": {
"type": "string"
}
},
"required": [
@ -52,11 +55,7 @@
"type": "array",
"items": {}
}
},
"required": [
"mode",
"options"
]
}
},
"dns": {
"type": "object",
@ -72,11 +71,7 @@
}
]
}
},
"required": [
"search",
"servers"
]
}
},
"ports": {
"type": "object",
@ -97,11 +92,7 @@
}
]
}
},
"required": [
"tcp",
"udp"
]
}
},
"env": {
"type": "object"
@ -132,11 +123,7 @@
}
]
}
},
"required": [
"add",
"drop"
]
}
},
"accounting": {
"type": "object",

View file

@ -186,6 +186,7 @@ class Memory:
reservation = maybe_or(val, "reservation", "")
swap = maybe_or(val, "swap", "")
if limit == "":
logger.log_warning("No limit set, memory config is not needed")
return cls("", "", "")
return cls(limit, reservation, swap)
@ -244,17 +245,26 @@ class Volume:
"""Container Volume."""
name: str
path: str
path: list
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
def from_json(cls, val: ConfigValue, logger: Log) -> list:
"""Create from JSON."""
if val is None:
return None
return []
if not isinstance(val, dict):
logger.log_warning("Volume key is present, but malformed.")
return None
return [cls(key, value) for key, value in val.items()]
logger.log_warning("Volume key is malformed.")
return []
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:
"""Check if this Volume is a named or a host volume."""
@ -262,7 +272,10 @@ class Volume:
def command(self) -> str:
"""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:
"""Create volume, if it does not exist."""
@ -298,17 +311,17 @@ class Secret:
name: str
secret_type: str
target: str
target: list
options: str
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
def from_json(cls, val: ConfigValue, logger: Log) -> list:
"""Create from JSON."""
if val is None:
return None
return []
if not isinstance(val, dict):
logger.log_warning("Secret key is present, but malformed!")
return None
logger.log_warning("Secret key is malformed!")
return []
secrets = []
for key in val:
if not isinstance(val[key], dict):
@ -316,26 +329,36 @@ class Secret:
continue
name = key
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", "")
if options is None:
options = ""
secrets.append(
cls(name, str(secret_type), str(target), str(options))
)
secrets.append(cls(name, str(secret_type), target, str(options)))
return secrets
def command(self) -> str:
"""Option for podman container create."""
cmd = (
f"--secret {self.name},:"
f"type={self.secret_type},"
f"target={self.target}"
)
# Not a password, ruff...
if self.secret_type == "mount" and self.options != "": # noqa: S105
cmd = f"{cmd},{self.options}"
cmd = ""
for target in self.target:
cmd += (
f"\t--secret {self.name},:"
f"type={self.secret_type},"
f"target={target}"
)
# Not a password, ruff...
has_option = self.secret_type == "mount" # noqa: S105
has_option = has_option and self.options != ""
if has_option:
cmd = f"{cmd},{self.options}"
cmd += " \\\n"
return cmd
@ -374,22 +397,29 @@ class Environment:
if val is None:
return cls([], "")
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([f"{key}='{value}'" for key, value in val.items()], "")
def command(self) -> str:
"""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:
"""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:
escaped_var = var.replace("'", "%b")
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:
"""Remove env file."""
@ -414,31 +444,45 @@ class Ports:
if val is None:
return cls([], [])
if not isinstance(val, dict):
logger.log_warning("Ports key is present, but malformed!")
logger.log_warning("Ports key is malformed!")
return cls([], [])
tcp_ports = maybe(val, "tcp")
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):
logger.log_warning("Key tcp_ports is not an array!")
return cls([], [])
logger.log_warning("tcp_ports configuration is malformed!")
return cls([], udp_ports)
if not isinstance(udp_ports, list):
logger.log_warning("Key udp_ports is not an array!")
return cls([], [])
logger.log_warning("udp_ports configuration is malformed!")
return cls(tcp_ports, [])
return cls(tcp_ports, udp_ports)
def command(self) -> str:
"""Option for podman container create."""
ports = ""
seperator = " \\\n"
ports += seperator.join(
[f"\t--port {port}/tcp" for port in self.tcp_ports]
tcp_ports = seperator.join(
[f"\t--publish {port}/tcp" for port in self.tcp_ports]
)
ports += seperator
ports += seperator.join(
[f"\t--port {port}/udp" for port in self.udp_ports]
udp_ports = seperator.join(
[f"\t--publish {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
@ -451,26 +495,31 @@ class Network:
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
"""Create from JSON."""
if val is None or not isinstance(val, dict):
logger.log_error("Network configuration is missing or malformed!")
if val is None:
return cls("", [])
if not isinstance(val, dict):
logger.log_warning("Network configuration is malformed!")
return cls("", [])
mode = maybe(val, "mode")
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!"
logger.log_error(err)
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:
"""Option for podman container create."""
if self.mode == "":
return ""
cmd = f"--network={self.mode}"
cmd = f"\t--network={self.mode}"
opts = ",".join(self.options)
if opts != "":
cmd += f":{opts}"
return cmd
return cmd + " \\\n"
@dataclass
@ -480,6 +529,7 @@ class Image:
registry: str
image: str
tag: str
cmd: str
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> Self | None:
@ -490,10 +540,13 @@ class Image:
registry = maybe_or(val, "registry", "")
image = maybe_or(val, "image", "")
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:
"""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}"
@ -505,13 +558,13 @@ class Capability:
mode: str
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> list | None:
def from_json(cls, val: ConfigValue, logger: Log) -> list:
"""Create from JSON."""
if val is None or not isinstance(val, dict):
logger.log_warning(
"Capabilities key is either missing or malformed!"
)
return None
if val is None:
return []
if not isinstance(val, dict):
logger.log_warning("Capabilities key is malformed!")
return []
add = [cls(value, "add") for value in val["add"]]
drop = [cls(value, "drop") for value in val["drop"]]
return add + drop
@ -531,49 +584,133 @@ class Dns:
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
"""Create from JSON."""
if val is None or not isinstance(val, dict):
logger.log_error("DNS Key is either missing or malformed!")
if val is None:
return cls([], "")
if not isinstance(val, dict):
logger.log_warning("DNS Key is malformed!")
return cls([], "")
search = maybe_or(val, "search", "")
servers = maybe(val, "servers")
if not isinstance(servers, list):
logger.log_error("Servers key is not an array!")
return cls([], "")
return cls(servers, str(search))
logger.log_warning("Servers key is not an array!")
return cls([], search)
return cls(servers, search)
def command(self) -> str:
"""Option for podman container create."""
if len(self.servers) == 0 and self.search == "":
return ""
if len(self.servers) == 0:
return f"--dns-search={self.search}"
return f"\t--dns-search={self.search} \\\n"
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)
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:
"""Container."""
name: str
image: Image
privileged: bool
read_only: bool
replace: bool
restart: str
pull_policy: str
timezone: str
network: Network
dns: Dns
ports: Ports
env: Environment | None
secrets: list | None
volumes: list | None
capabilities: list | None
ct_opts: ContainerOptions
ct_network: ContainerNetwork
env: Environment
secrets: list
volumes: list
capabilities: list
accounting: Accounting
def __init__(self, json: dict, logger: Log | None = None) -> None:
@ -583,20 +720,30 @@ class Container:
name = maybe(json, "name")
if name is None:
logger.log_error("No container name set, aborting!")
return
raise ConfigError("Container has no name")
image = maybe(json, "image")
if image is None:
logger.log_error("No image set, aborting!")
return
privileged = maybe(json, "privileged")
read_only = maybe(json, "read_only")
replace = maybe(json, "replace")
pull_policy = maybe_or(json, "pull_policy", "always")
restart = maybe_or(json, "restart", "no")
timezone = maybe_or(json, "timezone", "local")
network = maybe(json, "network")
dns = maybe(json, "dns")
ports = maybe(json, "ports")
raise ConfigError("Container has no image")
self.image = Image.from_json(image, logger)
image_valid = True
if self.image.image == "":
logger.log_error("Image has no image set!")
image_valid = False
if self.image.registry == "":
logger.log_error("Image has no registry set!")
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")
secrets = maybe(json, "secrets")
volumes = maybe(json, "volumes")
@ -604,18 +751,11 @@ class Container:
accounting = maybe(json, "accounting")
self.name = str(name)
self.image = Image.from_json(image, logger)
self.privileged = privileged is not None and bool(privileged)
self.read_only = read_only is not None and bool(read_only)
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.ct_opts = ct_opts
self.ct_network = ContainerNetwork.from_json(json, logger)
self.env = Environment.from_json(env, logger)
self.env.file = "/var/lib/containerctl/containers/"
self.env.file += f"{self.name}/environment"
self.env.file = "/var/lib/containerctl/environment-files/"
self.env.file += f"{self.name}"
self.secrets = Secret.from_json(secrets, logger)
self.volumes = Volume.from_json(volumes, logger)
self.capabilities = Capability.from_json(capabilities, logger)
@ -648,26 +788,15 @@ class Container:
def create_container(self) -> str:
"""Generate podman container create command."""
cmd = f"# Create container {self.name}\n"
cmd += "podman container crate \\\n"
cmd += "podman container create \\\n"
cmd += f"\t--name={self.name} \\\n"
if self.privileged:
cmd += "\t--privileged \\\n"
if self.replace:
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"
cmd += f"{self.ct_opts.command()}"
cmd += f"{self.ct_network.command()}"
cmd += f"{self.env.command()}"
for secret in self.secrets:
cmd += f"\t{secret.command()} \\\n"
cmd += f"{secret.command()}"
for volume in self.volumes:
cmd += f"\t{volume.command()} \\\n"
cmd += f"{volume.command()}"
for capability in self.capabilities:
cmd += f"\t{capability.command()} \\\n"
cmd += f"{self.accounting.command()}"

View file

@ -14,7 +14,7 @@ from pathlib import Path
from container import ConfigError, Container
from log import Log
GENERATE_VERSION = "0.0.4"
GENERATE_VERSION = "0.0.15"
HEADER = f"""#!/bin/sh
# This script was generated by containerctl v{GENERATE_VERSION}
# Report bugs with _this script_ to <tenno+containerctl@suij.in>
@ -76,10 +76,12 @@ def main() -> None:
if len(sys.argv) > log_threshold:
base = sys.argv[3]
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:
logger.log_error(f"{config_file} is invalid, aborting!")
logger.log_error(f"{conf.name} is invalid, aborting!")
sys.exit(1)
logger.set_prefix(conf.name)
ct = create_container_from_config(data, logger)
if ct is None:
sys.exit(1)
@ -109,7 +111,7 @@ def main() -> None:
script_content += ct.create_environment()
script_content += method()
if script_content == "":
script_content = "true"
script_content = "true\n"
with s.open("w+", encoding="utf-8") as f:
f.write(HEADER)
f.write(script_content)

View file

@ -15,6 +15,7 @@ class Log:
messages: list = []
logfile: Path
prefix: str = ""
def __init__(self, path: str) -> None:
"""Init for Log."""
@ -25,15 +26,19 @@ class Log:
def log_error(self, msg: str) -> None:
"""Log an error."""
now = self.timestamp()
prefix = "EE"
log_message = f"[{now}] ({prefix}) {msg}"
prefix = "(EE)"
if self.prefix != "":
prefix += f" {self.prefix}:"
log_message = f"[{now}] {prefix} {msg}"
self.write_message(log_message)
def log_warning(self, msg: str) -> None:
"""Log a warning."""
now = self.timestamp()
prefix = "WW"
log_message = f"[{now}] ({prefix}) {msg}"
prefix = "(WW)"
if self.prefix != "":
prefix += f" {self.prefix}:"
log_message = f"[{now}] {prefix} {msg}"
self.write_message(log_message)
def write_message(self, msg: str) -> None:
@ -49,3 +54,7 @@ class Log:
return datetime.datetime.now(tz=datetime.UTC).strftime(
"%Y-%m-%d %H:%M:%S",
)
def set_prefix(self, prefix: str) -> None:
"""Set a prefix."""
self.prefix = prefix