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": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"network": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -134,6 +137,66 @@
|
|||
"add",
|
||||
"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": [
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"replace": false,
|
||||
"pull_policy": "always",
|
||||
"restart": "always",
|
||||
"timezone": "Etc/UTC",
|
||||
"network": {
|
||||
"mode": "podman1",
|
||||
"options": []
|
||||
|
@ -42,11 +43,34 @@
|
|||
}
|
||||
},
|
||||
"volumes": {
|
||||
"etc": "/etc:ro,noexec",
|
||||
"/etc": "/etc:ro,noexec",
|
||||
"var": "/var"
|
||||
},
|
||||
"capabilities": {
|
||||
"add": [ "NET_RAW" ],
|
||||
"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}"
|
||||
|
||||
|
||||
def maybe(json: dict, key: str) -> str | dict | list | bool | None:
|
||||
def maybe(json: dict, key: str) -> ConfigValue:
|
||||
"""Maybe get a value."""
|
||||
try:
|
||||
return json[key]
|
||||
|
@ -37,6 +37,14 @@ def maybe(json: dict, key: str) -> str | dict | list | bool | 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:
|
||||
"""Remove sequential whitespace."""
|
||||
s = s.replace("\t ", "\t")
|
||||
|
@ -46,6 +54,207 @@ def trim(s: str) -> str:
|
|||
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
|
||||
class Volume:
|
||||
"""Container Volume."""
|
||||
|
@ -122,9 +331,9 @@ class Secret:
|
|||
logger.log_warning(f"Secret {key} is malformed!")
|
||||
continue
|
||||
name = key
|
||||
secret_type = maybe(val[key], "type")
|
||||
target = maybe(val[key], "target")
|
||||
options = maybe(val[key], "options")
|
||||
secret_type = maybe_or(val[key], "type", "")
|
||||
target = maybe_or(val[key], "target", "")
|
||||
options = maybe_or(val[key], "options", "")
|
||||
if options is None:
|
||||
options = ""
|
||||
secrets.append(
|
||||
|
@ -294,9 +503,9 @@ class Image:
|
|||
if val is None or not isinstance(val, dict):
|
||||
logger.log_error("Image key either not present or malformed!")
|
||||
return None
|
||||
registry = maybe(val, "registry")
|
||||
image = maybe(val, "image")
|
||||
tag = maybe(val, "tag")
|
||||
registry = maybe_or(val, "registry", "")
|
||||
image = maybe_or(val, "image", "")
|
||||
tag = maybe_or(val, "tag", "")
|
||||
return cls(str(registry), str(image), str(tag))
|
||||
|
||||
def command(self) -> str:
|
||||
|
@ -341,7 +550,7 @@ class Dns:
|
|||
if val is None or not isinstance(val, dict):
|
||||
logger.log_error("DNS Key is either missing or malformed!")
|
||||
return cls([], "")
|
||||
search = maybe(val, "search")
|
||||
search = maybe_or(val, "search", "")
|
||||
servers = maybe(val, "servers")
|
||||
if not isinstance(servers, list):
|
||||
logger.log_error("Servers key is not an array!")
|
||||
|
@ -373,6 +582,7 @@ class Container:
|
|||
replace: bool
|
||||
restart: str
|
||||
pull_policy: str
|
||||
timezone: str
|
||||
network: Network
|
||||
dns: Dns
|
||||
ports: Ports
|
||||
|
@ -380,6 +590,7 @@ class Container:
|
|||
secrets: list | None
|
||||
volumes: list | None
|
||||
capabilities: list | None
|
||||
accounting: Accounting
|
||||
|
||||
def __init__(self, json: dict, logger: Log | None = None) -> None:
|
||||
"""Create from JSON."""
|
||||
|
@ -396,12 +607,9 @@ class Container:
|
|||
privileged = maybe(json, "privileged")
|
||||
read_only = maybe(json, "read_only")
|
||||
replace = maybe(json, "replace")
|
||||
pull_policy = maybe(json, "pull_policy")
|
||||
if pull_policy is None:
|
||||
pull_policy = "always"
|
||||
restart = maybe(json, "restart")
|
||||
if restart is None:
|
||||
restart = "no"
|
||||
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")
|
||||
|
@ -409,6 +617,7 @@ class Container:
|
|||
secrets = maybe(json, "secrets")
|
||||
volumes = maybe(json, "volumes")
|
||||
capabilities = maybe(json, "capabilities")
|
||||
accounting = maybe(json, "accounting")
|
||||
self.name = str(name)
|
||||
self.image = Image.from_json(image, logger)
|
||||
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.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)
|
||||
|
@ -425,6 +635,7 @@ class Container:
|
|||
self.secrets = Secret.from_json(secrets, logger)
|
||||
self.volumes = Volume.from_json(volumes, logger)
|
||||
self.capabilities = Capability.from_json(capabilities, logger)
|
||||
self.accounting = Accounting.from_json(accounting, logger)
|
||||
|
||||
def create_volumes(self) -> str:
|
||||
"""Generate podman volume create commands."""
|
||||
|
@ -463,6 +674,7 @@ class Container:
|
|||
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()}"
|
||||
|
@ -474,6 +686,7 @@ class Container:
|
|||
cmd += f"\t{volume.command()} \\\n"
|
||||
for capability in self.capabilities:
|
||||
cmd += f"\t{capability.command()} \\\n"
|
||||
cmd += f"{self.accounting.command()}"
|
||||
cmd += f"\t{self.image.command()}\n"
|
||||
return cmd
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from pathlib import Path
|
|||
from container import ConfigError, Container
|
||||
from log import Log
|
||||
|
||||
GENERATE_VERSION = "0.0.2"
|
||||
GENERATE_VERSION = "0.0.4"
|
||||
HEADER = f"""#!/bin/sh
|
||||
# This script was generated by containerctl v{GENERATE_VERSION}
|
||||
# Report bugs with _this script_ to <tenno+containerctl@suij.in>
|
||||
|
|
Loading…
Add table
Reference in a new issue