Skip to content

Commit

Permalink
Merge pull request #124 from ydf/master
Browse files Browse the repository at this point in the history
continue merge the implemented --entrypoint and --rm
lavie authored Jan 10, 2025
2 parents 396ddea + cd4b774 commit b8d376b
Showing 4 changed files with 117 additions and 94 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -81,6 +81,8 @@ Probably **shouldn't use this in production** yet. If you do, double check that
print container ID
--device list Add a host device to the container
--dns list Set custom DNS servers
--entrypoint string Overwrite the default ENTRYPOINT
of the image
-e, --env list Set environment variables
--expose list Expose a port or a range of ports
-h, --hostname string Container host name
@@ -156,8 +158,6 @@ Probably **shouldn't use this in production** yet. If you do, double check that
--disable-content-trust Skip image verification (default true)
--dns-option list Set DNS options
--dns-search list Set custom DNS search domains
--entrypoint string Overwrite the default ENTRYPOINT
of the image
--env-file list Read in a file of environment variables
--group-add list Add additional groups to join
--health-cmd string Command to run to check health
6 changes: 6 additions & 0 deletions fixtures.sh
Original file line number Diff line number Diff line change
@@ -88,3 +88,9 @@ sudocker run -d --name runlike_fixture6 \
-p 10.10.0.1:602:600/udp \
runlike_fixture \
bash -c 'bash sleep.sh'

sudocker run -d --name runlike_fixture7 \
--rm \
--entrypoint /bin/bash \
runlike_fixture \
sleep.sh
96 changes: 53 additions & 43 deletions runlike/inspector.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import sys
from subprocess import (
check_output,
STDOUT,
CalledProcessError
)
from subprocess import check_output, STDOUT, CalledProcessError
from json import loads
from shlex import quote

@@ -26,14 +22,14 @@ def __init__(self, container=None, no_name=None, pretty=None):
def inspect(self):
try:
output = check_output(
["docker", "container", "inspect", self.container],
stderr=STDOUT)
self.container_facts = loads(output.decode('utf8', 'strict'))
["docker", "container", "inspect", self.container], stderr=STDOUT
)
self.container_facts = loads(output.decode("utf8", "strict"))
image_hash = self.get_container_fact("Image")
output = check_output(
["docker", "image", "inspect", image_hash],
stderr=STDOUT)
self.image_facts = loads(output.decode('utf8', 'strict'))
["docker", "image", "inspect", image_hash], stderr=STDOUT
)
self.image_facts = loads(output.decode("utf8", "strict"))
except CalledProcessError as e:
if b"No such container" in e.output:
die(f"No such container {self.container}")
@@ -83,45 +79,53 @@ def parse_user(self):

def parse_macaddress(self):
try:
mac_address = self.get_container_fact("Config.MacAddress") or self.get_container_fact("NetworkSettings.MacAddress") or {}
mac_address = (
self.get_container_fact("Config.MacAddress")
or self.get_container_fact("NetworkSettings.MacAddress")
or {}
)
if mac_address:
self.options.append(f"--mac-address={mac_address}")
except Exception:
pass

def parse_ports(self):
ports = self.get_container_fact("NetworkSettings.Ports") or {}
ports = self.get_container_fact("NetworkSettings.Ports") or {}
ports.update(self.get_container_fact("HostConfig.PortBindings") or {})

if ports:
for container_port_and_protocol, options_loop in ports.items():
container_port, protocol = container_port_and_protocol.split('/')
protocol_part = '' if protocol == 'tcp' else '/udp'
option_part = '-p '
host_port_part = ''
hostname_part = ''
container_port, protocol = container_port_and_protocol.split("/")
protocol_part = "" if protocol == "tcp" else "/udp"
option_part = "-p "
host_port_part = ""
hostname_part = ""

if options_loop is None:
# --expose
option_part = '--expose='
option_part = "--expose="

self.options.append(f"{option_part}{hostname_part}{host_port_part}{container_port}{protocol_part}")
self.options.append(
f"{option_part}{hostname_part}{host_port_part}{container_port}{protocol_part}"
)
else:
for host in options_loop:
# -p
host_ip = host['HostIp']
host_port = host['HostPort']
host_ip = host["HostIp"]
host_port = host["HostPort"]

if host_port != '0' and host_port != '':
if host_port != "0" and host_port != "":
host_port_part = f"{host_port}:"

if host_ip not in ['0.0.0.0', '::', '']:
if host_ip not in ["0.0.0.0", "::", ""]:
hostname_part = f"{host_ip}:"

self.options.append(f"{option_part}{hostname_part}{host_port_part}{container_port}{protocol_part}")

if self.options[-1] == self.options[-2] : self.options.pop()
self.options.append(
f"{option_part}{hostname_part}{host_port_part}{container_port}{protocol_part}"
)

if self.options[-1] == self.options[-2]:
self.options.pop()

def parse_links(self):
links = self.get_container_fact("HostConfig.Links")
@@ -157,9 +161,10 @@ def parse_restart(self):
return
elif restart in ["no"]:
return
elif restart == 'on-failure':
elif restart == "on-failure":
max_retries = self.get_container_fact(
"HostConfig.RestartPolicy.MaximumRetryCount")
"HostConfig.RestartPolicy.MaximumRetryCount"
)
if max_retries > 0:
restart += f":{max_retries}"
self.options.append(f"--restart={restart}")
@@ -170,11 +175,11 @@ def parse_devices(self):
return
device_options = set()
for device_spec in devices:
host = device_spec['PathOnHost']
container = device_spec['PathInContainer']
perms = device_spec['CgroupPermissions']
host = device_spec["PathOnHost"]
container = device_spec["PathInContainer"]
perms = device_spec["CgroupPermissions"]
spec = f"{host}:{container}"
if perms != 'rwm':
if perms != "rwm":
spec += f":{perms}"
device_options.add(f"--device {spec}")

@@ -197,7 +202,7 @@ def parse_log(self):
log_type = self.get_container_fact("HostConfig.LogConfig.Type")
log_opts = self.get_container_fact("HostConfig.LogConfig.Config") or {}
log_options = set()
if log_type != 'json-file':
if log_type != "json-file":
log_options.add(f"--log-driver={log_type}")
if log_opts:
for key, value in log_opts.items():
@@ -221,12 +226,18 @@ def parse_runtime(self):
def parse_memory(self):
memory = self.get_container_fact("HostConfig.Memory")
if memory:
self.options.append(f"--memory=\"{memory}\"")
self.options.append(f'--memory="{memory}"')

def parse_memory_reservation(self):
memory_reservation = self.get_container_fact("HostConfig.MemoryReservation")
if memory_reservation:
self.options.append(f"--memory-reservation=\"{memory_reservation}\"")
self.options.append(f'--memory-reservation="{memory_reservation}"')

def parse_entrypoint(self):
entrypoints = self.get_container_fact("Config.Entrypoint") or []
image_entrypoints = self.get_image_fact("Config.Entrypoint") or []
if len(entrypoints) > 0 and entrypoints != image_entrypoints:
self.options.append("--entrypoint %s" % entrypoints[0])

def format_cli(self):
image = self.get_container_fact("Config.Image")
@@ -240,6 +251,7 @@ def format_cli(self):
self.parse_macaddress()
self.parse_pid()
self.parse_cpuset()
self.parse_entrypoint()

self.multi_option("Config.Env", "env")
self.multi_option("HostConfig.Binds", "volume")
@@ -251,7 +263,7 @@ def format_cli(self):
network_mode = self.get_container_fact("HostConfig.NetworkMode")
if network_mode != "default":
self.options.append(f"--network={network_mode}")
privileged = self.get_container_fact('HostConfig.Privileged')
privileged = self.get_container_fact("HostConfig.Privileged")
if privileged:
self.options.append("--privileged")

@@ -272,25 +284,23 @@ def format_cli(self):
self.options.append("--detach=true")

if self.get_container_fact("Config.Tty"):
self.options.append('-t')
self.options.append("-t")

if self.get_container_fact("HostConfig.AutoRemove"):
self.options.append('--rm')
self.options.append("--rm")

parameters = []
if self.options:
parameters += self.options
parameters.append(image)

cmd_parts = self.get_container_fact("Config.Cmd")

if cmd_parts:
# NOTE: pipes.quote() performs syntactically correct
# quoting and replace operation below is needed just for
# aesthetic reasons and visual similarity with old output.
quoted = [
quote(p).replace("'\"'\"'", r"\'")
for p in cmd_parts
]
quoted = [quote(p).replace("'\"'\"'", r"\'") for p in cmd_parts]
command = " ".join(quoted)
parameters.append(command)

105 changes: 56 additions & 49 deletions test_runlike.py
Original file line number Diff line number Diff line change
@@ -6,15 +6,17 @@
from runlike.runlike import cli
from typing import List


def setUpModule():
check_output("./fixtures.sh")


class BaseTest(unittest.TestCase):
@classmethod
def start_runlike(cls, args: List[str]):
runner = CliRunner()
cls.outputs = [""] * 7
for i in range(1, 7):
cls.outputs = [""] * 8
for i in range(1, 8):
result = runner.invoke(cli, args + [f"runlike_fixture{i}"])
assert result.exit_code == 0, "runlike did not finish successfully"
cls.outputs[i] = result.output
@@ -31,6 +33,7 @@ def dont_expect_substr(self, substr, fixture_index=1):
hay = self.outputs[fixture_index]
self.assertNotIn(substr, hay)


class TestRunlike(BaseTest):
@classmethod
def setUpClass(cls):
@@ -65,98 +68,97 @@ def test_host_volumes(self):
self.expect_substr("--volume=%s:/workdir" % pipes.quote(cur_dir))

def test_no_host_volume(self):
self.expect_substr('--volume=/random_volume')
self.expect_substr("--volume=/random_volume")

def test_tty(self):
self.expect_substr('-t \\')
self.dont_expect_substr('-t \\', 2)
self.expect_substr("-t \\")
self.dont_expect_substr("-t \\", 2)

def test_autoremove(self):
self.expect_substr('--rm \\', 5)
self.expect_substr("--rm \\", 5)

def test_restart_always(self):
self.expect_substr('--restart=always \\')
self.expect_substr("--restart=always \\")

def test_restart_on_failure(self):
self.expect_substr('--restart=on-failure \\', 2)
self.expect_substr("--restart=on-failure \\", 2)

def test_restart_with_max(self):
self.expect_substr('--restart=on-failure:3 \\', 3)
self.expect_substr("--restart=on-failure:3 \\", 3)

def test_restart_not_present(self):
self.dont_expect_substr('--restart', 4)
self.dont_expect_substr("--restart", 4)

def test_hostname(self):
self.expect_substr('--hostname=Essos \\')
self.expect_substr("--hostname=Essos \\")

def test_hostname_not_present(self):
self.dont_expect_substr('--hostname \\', 2)
self.dont_expect_substr("--hostname \\", 2)

def test_network_mode(self):
self.dont_expect_substr('--network=host', 1)
self.dont_expect_substr('--network=runlike_fixture_bridge', 1)
self.expect_substr('--network=host', 2)
self.expect_substr('--network=runlike_fixture_bridge', 3)
self.dont_expect_substr("--network=host", 1)
self.dont_expect_substr("--network=runlike_fixture_bridge", 1)
self.expect_substr("--network=host", 2)
self.expect_substr("--network=runlike_fixture_bridge", 3)

def test_privileged_mode(self):
self.expect_substr('--privileged \\')
self.expect_substr("--privileged \\")

def test_privileged_not_present(self):
self.dont_expect_substr('--privileged \\', 2)
self.dont_expect_substr("--privileged \\", 2)

def test_multi_labels(self):
self.expect_substr("--label='com.example.environment=test' \\", 1)
self.expect_substr(
"--label='com.example.notescaped=$KEEP_DOLLAR' \\", 1)
self.expect_substr("--label='com.example.notescaped=$KEEP_DOLLAR' \\", 1)
self.dont_expect_substr("--label='ImageLabel", 1)

def test_one_label(self):
self.expect_substr("--label='com.example.version=1' \\", 2)
self.dont_expect_substr("--label='ImageLabel", 2)

def test_labels_not_present(self):
self.dont_expect_substr('--label', 3)
self.dont_expect_substr("--label", 3)

def test_extra_hosts(self):
self.expect_substr('--add-host hostname2:127.0.0.2 \\', 1)
self.expect_substr('--add-host hostname3:127.0.0.3 \\', 1)
self.expect_substr("--add-host hostname2:127.0.0.2 \\", 1)
self.expect_substr("--add-host hostname3:127.0.0.3 \\", 1)

def test_extra_hosts_not_present(self):
self.dont_expect_substr('--add-host', 2)
self.dont_expect_substr("--add-host", 2)

def test_log_driver_default_no_opts(self):
self.dont_expect_substr('--log-driver', 2)
self.dont_expect_substr('--log-opt', 2)
self.dont_expect_substr("--log-driver", 2)
self.dont_expect_substr("--log-opt", 2)

def test_log_driver_default_with_opts(self):
self.dont_expect_substr('--log-driver', 3)
self.expect_substr('--log-opt mode=non-blocking \\', 3)
self.expect_substr('--log-opt max-buffer-size=4m \\', 3)
self.dont_expect_substr("--log-driver", 3)
self.expect_substr("--log-opt mode=non-blocking \\", 3)
self.expect_substr("--log-opt max-buffer-size=4m \\", 3)

def test_log_driver_present(self):
self.expect_substr('--log-driver=fluentd \\')
self.expect_substr("--log-driver=fluentd \\")

def test_log_driver_options_present(self):
self.expect_substr('--log-opt fluentd-async-connect=true \\')
self.expect_substr('--log-opt tag=docker.runlike \\')
self.expect_substr("--log-opt fluentd-async-connect=true \\")
self.expect_substr("--log-opt tag=docker.runlike \\")

def test_links(self):
self.expect_substr('--link runlike_fixture4:alias_of4 \\', 5)
self.expect_substr('--link runlike_fixture1 \\', 5)
self.expect_substr("--link runlike_fixture4:alias_of4 \\", 5)
self.expect_substr("--link runlike_fixture1 \\", 5)

def test_command(self):
self.dont_expect_substr('/bin/bash', 1)
self.expect_substr('/bin/bash sleep.sh', 2)
self.dont_expect_substr("/bin/bash sleep.sh", 1)
self.expect_substr("/bin/bash sleep.sh", 2)
self.expect_substr("bash -c 'bash sleep.sh'", 3)
self.expect_substr(r"bash -c 'bash \'sleep.sh\'", 4)

def test_user(self):
self.expect_substr('--user=daemon')
self.dont_expect_substr('--user', 2)
self.expect_substr("--user=daemon")
self.dont_expect_substr("--user", 2)

def test_mac_address(self):
self.expect_substr('--mac-address=6a:00:01:ad:d9:e0', 4)
self.dont_expect_substr('--mac-address', 2)
self.expect_substr("--mac-address=6a:00:01:ad:d9:e0", 4)
self.dont_expect_substr("--mac-address", 2)

def test_env(self):
val = '''FOO=thing="quoted value with 'spaces' and 'single quotes'"'''
@@ -174,14 +176,14 @@ def test_devices(self):

def test_workdir(self):
self.expect_substr("--workdir=/workdir")
self.dont_expect_substr('--workdir', 2)
self.dont_expect_substr("--workdir", 2)

def test_runtime(self):
self.expect_substr('--runtime=runc')
self.expect_substr("--runtime=runc")

def test_pid_mode(self):
self.expect_substr('--pid host', 2)
self.dont_expect_substr('--pid')
self.expect_substr("--pid host", 2)
self.dont_expect_substr("--pid")

def test_memory(self):
self.expect_substr('--memory="2147483648"')
@@ -190,19 +192,24 @@ def test_memory_reservation(self):
self.expect_substr('--memory-reservation="1610612736"')

def test_cpuset(self):
self.expect_substr('--cpuset-cpus=0', 3)
self.expect_substr('--cpuset-mems=0', 3)
self.expect_substr("--cpuset-cpus=0", 3)
self.expect_substr("--cpuset-mems=0", 3)

def test_starts_with_docker_run(self):
self.starts_with('docker run ')
self.starts_with("docker run ")

def test_name(self):
self.expect_substr('--name=runlike_fixture1')
self.expect_substr("--name=runlike_fixture1")

def test_entrypoint(self):
self.expect_substr("--entrypoint /bin/bash", 7)
self.dont_expect_substr("--entrypoint", 6)


class TestRunlikeNoName(BaseTest):
@classmethod
def setUpClass(cls):
cls.start_runlike(["--no-name"])

def test_no_name(self):
self.dont_expect_substr('--name')
self.dont_expect_substr("--name")

0 comments on commit b8d376b

Please sign in to comment.