1
0
Fork 0

Compare commits

..

No commits in common. "main" and "0.0.9" have entirely different histories.
main ... 0.0.9

5 changed files with 105 additions and 233 deletions

View file

@ -11,36 +11,9 @@ LOGDIR="/var/log/containerctl"
TODAY="$(date '+%F')"
LOG="${LOGDIR}/${TODAY}"
get_python_path()
{
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
}
log_error()
{
printf '[%b] (EE) %b\n' "${TODAY}" "${@}" | tee -a "${LOG}" 2> /dev/null
printf '[%b] (EE) %b\n' "${TODAY}" "${@}" | tee -a "${LOG}"
}
list_containers()
@ -133,7 +106,12 @@ generate_container()
exit 1
fi
mypython="$(get_python_path)"
mypython="python3"
pyver="$(/usr/bin/env "${mypython}" -c 'import sys; print(sys.version_info.minor)')"
if [ "${pyver}" -gt "11" ]
then
mypython="python3.11"
fi
/usr/bin/env "${mypython}" "${BASEDIR}/generate/generate.py" \
"${CONFIGDIR}/${config}" "${LOG}" "${CONTAINERDIR}"
@ -152,17 +130,10 @@ generate_all()
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_container "${@}" ;;
"help") usage "${0}" ;;
"usage") usage "${0}" ;;
*) exec_script "${@}" ;;
esac

View file

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

View file

@ -186,7 +186,6 @@ 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)
@ -245,7 +244,7 @@ class Volume:
"""Container Volume."""
name: str
path: list
path: str
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> list:
@ -253,18 +252,9 @@ class Volume:
if val is None:
return []
if not isinstance(val, dict):
logger.log_warning("Volume key is malformed.")
logger.log_warning("Volume key is present, but 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)
return [cls(key, value) for key, value in val.items()]
def is_host_volume(self) -> bool:
"""Check if this Volume is a named or a host volume."""
@ -272,10 +262,7 @@ class Volume:
def command(self) -> str:
"""Option for podman container create."""
cmd = ""
for path in self.path:
cmd += f"\t--volume {self.name}:{path} \\\n"
return cmd
return f"--volume {self.name}:{self.path}"
def create(self) -> str:
"""Create volume, if it does not exist."""
@ -311,7 +298,7 @@ class Secret:
name: str
secret_type: str
target: list
target: str
options: str
@classmethod
@ -320,7 +307,7 @@ class Secret:
if val is None:
return []
if not isinstance(val, dict):
logger.log_warning("Secret key is malformed!")
logger.log_warning("Secret key is present, but malformed!")
return []
secrets = []
for key in val:
@ -329,36 +316,26 @@ class Secret:
continue
name = key
secret_type = maybe_or(val[key], "type", "")
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 = []
target = maybe_or(val[key], "target", "")
options = maybe_or(val[key], "options", "")
if options is None:
options = ""
secrets.append(cls(name, str(secret_type), target, str(options)))
secrets.append(
cls(name, str(secret_type), str(target), str(options))
)
return secrets
def command(self) -> str:
"""Option for podman container create."""
cmd = ""
for target in self.target:
cmd += (
f"\t--secret {self.name},:"
cmd = (
f"--secret {self.name},:"
f"type={self.secret_type},"
f"target={target}"
f"target={self.target}"
)
# Not a password, ruff...
has_option = self.secret_type == "mount" # noqa: S105
has_option = has_option and self.options != ""
if has_option:
if self.secret_type == "mount" and self.options != "": # noqa: S105
cmd = f"{cmd},{self.options}"
cmd += " \\\n"
return cmd
@ -397,7 +374,7 @@ class Environment:
if val is None:
return cls([], "")
if not isinstance(val, dict):
logger.log_warning("Environment key is malformed!")
logger.log_warning("Environment key is present, but malformed!")
return cls([], "")
return cls([f"{key}='{value}'" for key, value in val.items()], "")
@ -419,7 +396,7 @@ class Environment:
if cmd == "":
return ""
return header + f"printf '\\n' > {self.file}\n" + cmd
return header + cmd
def remove(self) -> str:
"""Remove env file."""
@ -444,7 +421,7 @@ class Ports:
if val is None:
return cls([], [])
if not isinstance(val, dict):
logger.log_warning("Ports key is malformed!")
logger.log_warning("Ports key is present, but malformed!")
return cls([], [])
tcp_ports = maybe(val, "tcp")
udp_ports = maybe(val, "udp")
@ -503,13 +480,13 @@ class Network:
return cls("", [])
mode = maybe(val, "mode")
options = maybe(val, "options")
if mode is None or not isinstance(mode, str):
if mode is None:
err = "Network configuration is missing or has malformed elements!"
logger.log_error(err)
return cls("", [])
if options is None or not isinstance(options, list):
return cls(mode, [])
return cls(mode, options)
return cls(str(mode), [])
return cls(str(mode), options)
def command(self) -> str:
"""Option for podman container create."""
@ -529,7 +506,6 @@ class Image:
registry: str
image: str
tag: str
cmd: str
@classmethod
def from_json(cls, val: ConfigValue, logger: Log) -> Self | None:
@ -540,13 +516,10 @@ class Image:
registry = maybe_or(val, "registry", "")
image = maybe_or(val, "image", "")
tag = maybe_or(val, "tag", "")
cmd = maybe_or(val, "command", "")
return cls(registry, image, tag, cmd)
return cls(str(registry), str(image), str(tag))
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}"
@ -593,9 +566,9 @@ class Dns:
search = maybe_or(val, "search", "")
servers = maybe(val, "servers")
if not isinstance(servers, list):
logger.log_warning("Servers key is not an array!")
return cls([], search)
return cls(servers, search)
logger.log_error("Servers key is not an array!")
return cls([], "")
return cls(servers, str(search))
def command(self) -> str:
"""Option for podman container create."""
@ -612,101 +585,20 @@ class Dns:
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
ct_opts: ContainerOptions
ct_network: ContainerNetwork
privileged: bool
read_only: bool
replace: bool
restart: str
pull_policy: str
timezone: str
network: Network
dns: Dns
ports: Ports
env: Environment
secrets: list
volumes: list
@ -720,30 +612,20 @@ class Container:
name = maybe(json, "name")
if name is None:
logger.log_error("No container name set, aborting!")
raise ConfigError("Container has no name")
return
image = maybe(json, "image")
if image is None:
logger.log_error("No image set, aborting!")
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?")
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")
env = maybe(json, "env")
secrets = maybe(json, "secrets")
volumes = maybe(json, "volumes")
@ -751,8 +633,15 @@ class Container:
accounting = maybe(json, "accounting")
self.name = str(name)
self.image = Image.from_json(image, logger)
self.ct_opts = ct_opts
self.ct_network = ContainerNetwork.from_json(json, 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.env = Environment.from_json(env, logger)
self.env.file = "/var/lib/containerctl/environment-files/"
self.env.file += f"{self.name}"
@ -790,13 +679,23 @@ class Container:
cmd = f"# Create container {self.name}\n"
cmd += "podman container create \\\n"
cmd += f"\t--name={self.name} \\\n"
cmd += f"{self.ct_opts.command()}"
cmd += f"{self.ct_network.command()}"
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"{self.network.command()}"
cmd += f"{self.dns.command()}"
cmd += f"{self.ports.command()}"
cmd += f"{self.env.command()}"
for secret in self.secrets:
cmd += f"{secret.command()}"
cmd += f"\t{secret.command()} \\\n"
for volume in self.volumes:
cmd += f"{volume.command()}"
cmd += f"\t{volume.command()} \\\n"
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.15"
GENERATE_VERSION = "0.0.9"
HEADER = f"""#!/bin/sh
# This script was generated by containerctl v{GENERATE_VERSION}
# Report bugs with _this script_ to <tenno+containerctl@suij.in>
@ -76,12 +76,10 @@ def main() -> None:
if len(sys.argv) > log_threshold:
base = sys.argv[3]
logger = Log(log_file)
conf = Path(config_file)
data = load_container_config(conf, logger)
data = load_container_config(Path(config_file), logger)
if data is None:
logger.log_error(f"{conf.name} is invalid, aborting!")
logger.log_error(f"{config_file} 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)

View file

@ -15,7 +15,6 @@ class Log:
messages: list = []
logfile: Path
prefix: str = ""
def __init__(self, path: str) -> None:
"""Init for Log."""
@ -26,19 +25,15 @@ class Log:
def log_error(self, msg: str) -> None:
"""Log an error."""
now = self.timestamp()
prefix = "(EE)"
if self.prefix != "":
prefix += f" {self.prefix}:"
log_message = f"[{now}] {prefix} {msg}"
prefix = "EE"
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)"
if self.prefix != "":
prefix += f" {self.prefix}:"
log_message = f"[{now}] {prefix} {msg}"
prefix = "WW"
log_message = f"[{now}] ({prefix}) {msg}"
self.write_message(log_message)
def write_message(self, msg: str) -> None:
@ -54,7 +49,3 @@ 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