Compare commits
19 commits
a601e76a93
...
5bcf2f846c
Author | SHA1 | Date | |
---|---|---|---|
5bcf2f846c | |||
058af68169 | |||
6767d4e3af | |||
e8d3cd96eb | |||
a8bf51020d | |||
3908ab3013 | |||
0a46bba629 | |||
18bb31cb96 | |||
3ed71b48ef | |||
487be8c49a | |||
658fc6465c | |||
1c2224cb19 | |||
8464196aaa | |||
4224112c62 | |||
1cd43e0c95 | |||
93245d5bc6 | |||
2a7bb0115c | |||
57b983e876 | |||
e100abf698 |
4 changed files with 460 additions and 160 deletions
|
@ -39,6 +39,9 @@
|
||||||
"restart": {
|
"restart": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"timezone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -134,6 +137,66 @@
|
||||||
"add",
|
"add",
|
||||||
"drop"
|
"drop"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"accounting": {
|
||||||
|
"type": "object",
|
||||||
|
"propeties": {
|
||||||
|
"cgroup": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"how": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"period": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quota": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"shares": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"number": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cpuset": {
|
||||||
|
"cpus": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mems": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"limit": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reservation": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"swap": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"replace": false,
|
"replace": false,
|
||||||
"pull_policy": "always",
|
"pull_policy": "always",
|
||||||
"restart": "always",
|
"restart": "always",
|
||||||
|
"timezone": "Etc/UTC",
|
||||||
"network": {
|
"network": {
|
||||||
"mode": "podman1",
|
"mode": "podman1",
|
||||||
"options": []
|
"options": []
|
||||||
|
@ -42,11 +43,34 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"volumes": {
|
"volumes": {
|
||||||
"etc": "/etc:ro,noexec",
|
"/etc": "/etc:ro,noexec",
|
||||||
"var": "/var"
|
"var": "/var"
|
||||||
},
|
},
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"add": [ "NET_RAW" ],
|
"add": [ "NET_RAW" ],
|
||||||
"drop": [ "CAP_SYS_ADMIN" ]
|
"drop": [ "CAP_SYS_ADMIN" ]
|
||||||
|
},
|
||||||
|
"accounting": {
|
||||||
|
"cgroup": {
|
||||||
|
"config": ["memory.high=1073741824"],
|
||||||
|
"parent": "/example-parent",
|
||||||
|
"namespace": "host",
|
||||||
|
"how": "enabled"
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"period": "100000",
|
||||||
|
"quota": "100000",
|
||||||
|
"shares": "1024",
|
||||||
|
"number": "4",
|
||||||
|
"cpuset": {
|
||||||
|
"cpus": "0-3,11-15",
|
||||||
|
"mems": "0,1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"limit": "16g",
|
||||||
|
"reservation": "512m",
|
||||||
|
"swap": "20g"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ class ConfigError(Exception):
|
||||||
return f"Configuration error: {self.message}"
|
return f"Configuration error: {self.message}"
|
||||||
|
|
||||||
|
|
||||||
def maybe(json: dict, key: str) -> str | dict | list | bool | None:
|
def maybe(json: dict, key: str) -> ConfigValue:
|
||||||
"""Maybe get a value."""
|
"""Maybe get a value."""
|
||||||
try:
|
try:
|
||||||
return json[key]
|
return json[key]
|
||||||
|
@ -37,6 +37,14 @@ def maybe(json: dict, key: str) -> str | dict | list | bool | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_or(json: dict, key: str, _or: ConfigValue) -> ConfigValue:
|
||||||
|
"""Maybe get a value, but return _or if it is None."""
|
||||||
|
val = maybe(json, key)
|
||||||
|
if val is None or not isinstance(val, type(_or)):
|
||||||
|
return _or
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
def trim(s: str) -> str:
|
def trim(s: str) -> str:
|
||||||
"""Remove sequential whitespace."""
|
"""Remove sequential whitespace."""
|
||||||
s = s.replace("\t ", "\t")
|
s = s.replace("\t ", "\t")
|
||||||
|
@ -46,6 +54,207 @@ def trim(s: str) -> str:
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cgroup:
|
||||||
|
"""cgroup Config."""
|
||||||
|
|
||||||
|
config: list
|
||||||
|
parent: str
|
||||||
|
namespace: str
|
||||||
|
how: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
if val is None:
|
||||||
|
return cls([], "", "", "")
|
||||||
|
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_warning("cgroup Config is invalid!")
|
||||||
|
return cls([], "", "", "")
|
||||||
|
|
||||||
|
config = maybe_or(val, "config", [])
|
||||||
|
parent = maybe_or(val, "parent", "")
|
||||||
|
namespace = maybe_or(val, "namespace", "")
|
||||||
|
how = maybe_or(val, "how", "")
|
||||||
|
|
||||||
|
if not isinstance(config, list):
|
||||||
|
logger.log_warning("Config key in cgroup Config is invalid!")
|
||||||
|
config = []
|
||||||
|
|
||||||
|
if not isinstance(parent, str):
|
||||||
|
logger.log_warning("Parent key in cgroup Config is invalid!")
|
||||||
|
parent = ""
|
||||||
|
|
||||||
|
if not isinstance(namespace, str):
|
||||||
|
logger.log_warning("Namespace key in cgroup Config is invalid!")
|
||||||
|
namespace = ""
|
||||||
|
|
||||||
|
if not isinstance(how, str):
|
||||||
|
logger.log_warning("How key in cgroup Config is invalid!")
|
||||||
|
how = ""
|
||||||
|
|
||||||
|
if how == "split" and parent != "":
|
||||||
|
logger.log_warning(
|
||||||
|
"Split cgroups can not be combined with a cgroup parent!"
|
||||||
|
)
|
||||||
|
parent = ""
|
||||||
|
|
||||||
|
return cls(config, parent, namespace, how)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Option for podman container create."""
|
||||||
|
cmd = ""
|
||||||
|
seperator = " \\\n"
|
||||||
|
cmd += seperator.join(
|
||||||
|
[f"\t--cgroup-conf={option}{seperator}" for option in self.config]
|
||||||
|
)
|
||||||
|
if self.parent != "":
|
||||||
|
cmd += f"\t--cgroup-parent={self.parent}{seperator}"
|
||||||
|
if self.namespace != "":
|
||||||
|
cmd += f"\t--cgroupns={self.namespace}{seperator}"
|
||||||
|
if self.how != "":
|
||||||
|
cmd += f"\t--cgroups={self.how}{seperator}"
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cpu:
|
||||||
|
"""CPU Accounting."""
|
||||||
|
|
||||||
|
period: str
|
||||||
|
quota: str
|
||||||
|
shares: str
|
||||||
|
number: str
|
||||||
|
cpuset_cpus: str
|
||||||
|
cpuset_mems: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
if val is None:
|
||||||
|
return cls("", "", "", "", "", "")
|
||||||
|
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_warning("cpu Config is invalid!")
|
||||||
|
return cls("", "", "", "", "", "")
|
||||||
|
|
||||||
|
period = maybe_or(val, "period", "")
|
||||||
|
quota = maybe_or(val, "quota", "")
|
||||||
|
shares = maybe_or(val, "shares", "")
|
||||||
|
number = maybe_or(val, "number", "")
|
||||||
|
cpuset = maybe_or(val, "cpuset", {})
|
||||||
|
cpuset_cpus = ""
|
||||||
|
cpuset_mems = ""
|
||||||
|
if len(cpuset) != 0:
|
||||||
|
cpuset_cpus += maybe_or(cpuset, "cpus", "")
|
||||||
|
cpuset_mems += maybe_or(cpuset, "mems", "")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
period,
|
||||||
|
quota,
|
||||||
|
shares,
|
||||||
|
number,
|
||||||
|
cpuset_cpus,
|
||||||
|
cpuset_mems,
|
||||||
|
)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Option for podman container create."""
|
||||||
|
cmd = ""
|
||||||
|
seperator = " \\\n"
|
||||||
|
if self.period != "":
|
||||||
|
cmd += f"\t--cpu-period={self.period}{seperator}"
|
||||||
|
if self.quota != "":
|
||||||
|
cmd += f"\t--cpu-quota={self.quota}{seperator}"
|
||||||
|
if self.shares != "":
|
||||||
|
cmd += f"\t--cpu-shares={self.shares}{seperator}"
|
||||||
|
if self.number != "":
|
||||||
|
cmd += f"\t--cpus={self.number}{seperator}"
|
||||||
|
if self.cpuset_cpus != "":
|
||||||
|
cmd += f"\t--cpuset-cpus={self.cpuset_cpus}{seperator}"
|
||||||
|
if self.cpuset_mems != "":
|
||||||
|
cmd += f"\t--cpuset-mems={self.cpuset_mems}{seperator}"
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Memory:
|
||||||
|
"""Memory accounting."""
|
||||||
|
|
||||||
|
limit: str
|
||||||
|
reservation: str
|
||||||
|
swap: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, val: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
if val is None:
|
||||||
|
return cls("", "", "")
|
||||||
|
|
||||||
|
if not isinstance(val, dict):
|
||||||
|
logger.log_warning("memory Config is invalid!")
|
||||||
|
return cls("", "", "")
|
||||||
|
|
||||||
|
limit = maybe_or(val, "limit", "")
|
||||||
|
reservation = maybe_or(val, "reservation", "")
|
||||||
|
swap = maybe_or(val, "swap", "")
|
||||||
|
if limit == "":
|
||||||
|
return cls("", "", "")
|
||||||
|
return cls(limit, reservation, swap)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Option for podman container create."""
|
||||||
|
if self.limit == "":
|
||||||
|
return ""
|
||||||
|
cmd = ""
|
||||||
|
seperator = " \\\n"
|
||||||
|
|
||||||
|
cmd += f"\t--memory={self.limit}{seperator}"
|
||||||
|
if self.reservation != "":
|
||||||
|
cmd += f"\t--memory-reservation={self.reservation}{seperator}"
|
||||||
|
if self.swap != "":
|
||||||
|
cmd += f"\t--memory-swap={self.swap}{seperator}"
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Accounting:
|
||||||
|
"""Resource Accounting."""
|
||||||
|
|
||||||
|
cgroup: Cgroup
|
||||||
|
cpu: Cpu
|
||||||
|
memory: Memory
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, data: ConfigValue, logger: Log) -> Self:
|
||||||
|
"""Create from JSON."""
|
||||||
|
if data is None:
|
||||||
|
return cls(
|
||||||
|
Cgroup.from_json(None, logger),
|
||||||
|
Cpu.from_json(None, logger),
|
||||||
|
Memory.from_json(None, logger),
|
||||||
|
)
|
||||||
|
|
||||||
|
cgroup_data = maybe(data, "cgroup")
|
||||||
|
cpu_data = maybe(data, "cpu")
|
||||||
|
memory_data = maybe(data, "memory")
|
||||||
|
cgroup = Cgroup.from_json(cgroup_data, logger)
|
||||||
|
cpu = Cpu.from_json(cpu_data, logger)
|
||||||
|
memory = Memory.from_json(memory_data, logger)
|
||||||
|
return cls(cgroup, cpu, memory)
|
||||||
|
|
||||||
|
def command(self) -> str:
|
||||||
|
"""Options for podman container create."""
|
||||||
|
cgroup = self.cgroup.command()
|
||||||
|
cpu = self.cpu.command()
|
||||||
|
memory = self.memory.command()
|
||||||
|
return cgroup + cpu + memory
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Volume:
|
class Volume:
|
||||||
"""Container Volume."""
|
"""Container Volume."""
|
||||||
|
@ -122,9 +331,9 @@ class Secret:
|
||||||
logger.log_warning(f"Secret {key} is malformed!")
|
logger.log_warning(f"Secret {key} is malformed!")
|
||||||
continue
|
continue
|
||||||
name = key
|
name = key
|
||||||
secret_type = maybe(val[key], "type")
|
secret_type = maybe_or(val[key], "type", "")
|
||||||
target = maybe(val[key], "target")
|
target = maybe_or(val[key], "target", "")
|
||||||
options = maybe(val[key], "options")
|
options = maybe_or(val[key], "options", "")
|
||||||
if options is None:
|
if options is None:
|
||||||
options = ""
|
options = ""
|
||||||
secrets.append(
|
secrets.append(
|
||||||
|
@ -294,9 +503,9 @@ class Image:
|
||||||
if val is None or not isinstance(val, dict):
|
if val is None or not isinstance(val, dict):
|
||||||
logger.log_error("Image key either not present or malformed!")
|
logger.log_error("Image key either not present or malformed!")
|
||||||
return None
|
return None
|
||||||
registry = maybe(val, "registry")
|
registry = maybe_or(val, "registry", "")
|
||||||
image = maybe(val, "image")
|
image = maybe_or(val, "image", "")
|
||||||
tag = maybe(val, "tag")
|
tag = maybe_or(val, "tag", "")
|
||||||
return cls(str(registry), str(image), str(tag))
|
return cls(str(registry), str(image), str(tag))
|
||||||
|
|
||||||
def command(self) -> str:
|
def command(self) -> str:
|
||||||
|
@ -341,7 +550,7 @@ class Dns:
|
||||||
if val is None or not isinstance(val, dict):
|
if val is None or not isinstance(val, dict):
|
||||||
logger.log_error("DNS Key is either missing or malformed!")
|
logger.log_error("DNS Key is either missing or malformed!")
|
||||||
return cls([], "")
|
return cls([], "")
|
||||||
search = maybe(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_error("Servers key is not an array!")
|
||||||
|
@ -373,6 +582,7 @@ class Container:
|
||||||
replace: bool
|
replace: bool
|
||||||
restart: str
|
restart: str
|
||||||
pull_policy: str
|
pull_policy: str
|
||||||
|
timezone: str
|
||||||
network: Network
|
network: Network
|
||||||
dns: Dns
|
dns: Dns
|
||||||
ports: Ports
|
ports: Ports
|
||||||
|
@ -380,6 +590,7 @@ class Container:
|
||||||
secrets: list | None
|
secrets: list | None
|
||||||
volumes: list | None
|
volumes: list | None
|
||||||
capabilities: list | None
|
capabilities: list | None
|
||||||
|
accounting: Accounting
|
||||||
|
|
||||||
def __init__(self, json: dict, logger: Log | None = None) -> None:
|
def __init__(self, json: dict, logger: Log | None = None) -> None:
|
||||||
"""Create from JSON."""
|
"""Create from JSON."""
|
||||||
|
@ -396,12 +607,9 @@ class Container:
|
||||||
privileged = maybe(json, "privileged")
|
privileged = maybe(json, "privileged")
|
||||||
read_only = maybe(json, "read_only")
|
read_only = maybe(json, "read_only")
|
||||||
replace = maybe(json, "replace")
|
replace = maybe(json, "replace")
|
||||||
pull_policy = maybe(json, "pull_policy")
|
pull_policy = maybe_or(json, "pull_policy", "always")
|
||||||
if pull_policy is None:
|
restart = maybe_or(json, "restart", "no")
|
||||||
pull_policy = "always"
|
timezone = maybe_or(json, "timezone", "local")
|
||||||
restart = maybe(json, "restart")
|
|
||||||
if restart is None:
|
|
||||||
restart = "no"
|
|
||||||
network = maybe(json, "network")
|
network = maybe(json, "network")
|
||||||
dns = maybe(json, "dns")
|
dns = maybe(json, "dns")
|
||||||
ports = maybe(json, "ports")
|
ports = maybe(json, "ports")
|
||||||
|
@ -409,6 +617,7 @@ class Container:
|
||||||
secrets = maybe(json, "secrets")
|
secrets = maybe(json, "secrets")
|
||||||
volumes = maybe(json, "volumes")
|
volumes = maybe(json, "volumes")
|
||||||
capabilities = maybe(json, "capabilities")
|
capabilities = maybe(json, "capabilities")
|
||||||
|
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.privileged = privileged is not None and bool(privileged)
|
||||||
|
@ -416,6 +625,7 @@ class Container:
|
||||||
self.replace = replace is not None and bool(replace)
|
self.replace = replace is not None and bool(replace)
|
||||||
self.pull_policy = str(pull_policy)
|
self.pull_policy = str(pull_policy)
|
||||||
self.restart = str(restart)
|
self.restart = str(restart)
|
||||||
|
self.timezone = str(timezone)
|
||||||
self.network = Network.from_json(network, logger)
|
self.network = Network.from_json(network, logger)
|
||||||
self.dns = Dns.from_json(dns, logger)
|
self.dns = Dns.from_json(dns, logger)
|
||||||
self.ports = Ports.from_json(ports, logger)
|
self.ports = Ports.from_json(ports, logger)
|
||||||
|
@ -425,6 +635,7 @@ class Container:
|
||||||
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)
|
||||||
|
self.accounting = Accounting.from_json(accounting, logger)
|
||||||
|
|
||||||
def create_volumes(self) -> str:
|
def create_volumes(self) -> str:
|
||||||
"""Generate podman volume create commands."""
|
"""Generate podman volume create commands."""
|
||||||
|
@ -463,6 +674,7 @@ class Container:
|
||||||
cmd += "\t--read-only \\\n"
|
cmd += "\t--read-only \\\n"
|
||||||
cmd += f"\t--restart={self.restart} \\\n"
|
cmd += f"\t--restart={self.restart} \\\n"
|
||||||
cmd += f"\t--pull={self.pull_policy} \\\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.network.command()} \\\n"
|
||||||
cmd += f"\t{self.dns.command()} \\\n"
|
cmd += f"\t{self.dns.command()} \\\n"
|
||||||
cmd += f"{self.ports.command()}"
|
cmd += f"{self.ports.command()}"
|
||||||
|
@ -474,6 +686,7 @@ class Container:
|
||||||
cmd += f"\t{volume.command()} \\\n"
|
cmd += f"\t{volume.command()} \\\n"
|
||||||
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"\t{self.image.command()}\n"
|
cmd += f"\t{self.image.command()}\n"
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
|
@ -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.2"
|
GENERATE_VERSION = "0.0.4"
|
||||||
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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue