diff --git a/broker/binds/containers.py b/broker/binds/containers.py index cf291e1c..4cef1aa7 100644 --- a/broker/binds/containers.py +++ b/broker/binds/containers.py @@ -1,5 +1,52 @@ """A collection of classes to ease interaction with Docker and Podman libraries.""" +HEADER_SIZE = 8 +STDOUT = 1 +STDERR = 2 + + +def demux_output(data_bytes): + """Demuxes the output of a container stream into stdout and stderr streams. + + Stream data is expected to be in the following format: + - 1 byte: stream type (1=stdout, 2=stderr) + - 3 bytes: padding + - 4 bytes: payload size (big-endian) + - N bytes: payload data + ref: https://docs.podman.io/en/latest/_static/api.html?version=v5.0#tag/containers/operation/ContainerAttachLibpod + + Args: + data_bytes: Bytes object containing the combined stream data. + + Returns: + A tuple containing two bytes objects: (stdout, stderr). + """ + stdout = b"" + stderr = b"" + while len(data_bytes) >= HEADER_SIZE: + # Extract header information + header, data_bytes = data_bytes[:HEADER_SIZE], data_bytes[HEADER_SIZE:] + stream_type = header[0] + payload_size = int.from_bytes(header[4:HEADER_SIZE], "big") + # Check if data is sufficient for payload + if len(data_bytes) < payload_size: + break # Incomplete frame, wait for more data + + # Extract and process payload + payload = data_bytes[:payload_size] + if stream_type == STDOUT: + stdout += payload + elif stream_type == STDERR: + stderr += payload + else: + # todo: Handle unexpected stream types + pass + + # Update data for next frame + data_bytes = data_bytes[payload_size:] + + return stdout, stderr + class ContainerBind: """A base class that provides common functionality for Docker and Podman containers.""" diff --git a/broker/helpers.py b/broker/helpers.py index 245ac3fa..6cf7b43a 100644 --- a/broker/helpers.py +++ b/broker/helpers.py @@ -20,6 +20,7 @@ import yaml from broker import exceptions, logger as b_log, settings +from broker.binds.containers import demux_output FilterTest = namedtuple("FilterTest", "haystack needle test") INVENTORY_LOCK = threading.Lock() @@ -512,8 +513,15 @@ def from_ssh(cls, stdout, channel): ) @classmethod - def from_duplexed_exec(cls, duplex_exec): - """Create a Result object from a duplexed exec object from the docker library.""" + def from_duplexed_exec(cls, duplex_exec, runtime=None): + """Create a Result object from a duplexed exec object from podman or docker.""" + if runtime == "podman": + stdout, stderr = demux_output(duplex_exec[1]) + return cls( + status=duplex_exec[0], + stdout=stdout.decode("utf-8"), + stderr=stderr.decode("utf-8"), + ) if duplex_exec.output[0]: stdout = duplex_exec.output[0].decode("utf-8") else: diff --git a/broker/hosts.py b/broker/hosts.py index ffc6c84b..036b8178 100644 --- a/broker/hosts.py +++ b/broker/hosts.py @@ -83,7 +83,8 @@ def session(self): if not isinstance(getattr(self, "_session", None), Session): # Check to see if we're a non-ssh-enabled Container Host if hasattr(self, "_cont_inst") and not self._cont_inst.ports.get(22): - self._session = ContainerSession(self) + runtime = "podman" if "podman" in str(self._cont_inst.client) else "docker" + self._session = ContainerSession(self, runtime=runtime) else: self.connect() return self._session diff --git a/broker/providers/container.py b/broker/providers/container.py index ac61c994..b8b27aad 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -127,7 +127,7 @@ def _find_ssh_port(port_map, ssh_port=22): elif isinstance(port_map, dict): # {'22/tcp': [{'HostIp': '', 'HostPort': '1337'}], for key, val in port_map.items(): - if key.startswith("22"): + if key.startswith("22") and isinstance(val, list): return val[0]["HostPort"] def _set_attributes(self, host_inst, broker_args=None, cont_inst=None): @@ -297,6 +297,8 @@ def run_container(self, container_host, **kwargs): @Provider.register_action("container_app") def execute(self, container_app, **kwargs): """Run a container and return the raw results.""" + if not kwargs.get("name"): + kwargs["name"] = self._gen_name() return self.runtime.execute(container_app, **kwargs) def run_wait_container(self, image_name, **kwargs): diff --git a/broker/session.py b/broker/session.py index 6bcdbb78..4abddfd9 100644 --- a/broker/session.py +++ b/broker/session.py @@ -59,8 +59,11 @@ def __init__(self, **kwargs): class ContainerSession: """An approximation of ssh-based functionality from the Session class.""" - def __init__(self, cont_inst): + def __init__(self, cont_inst, runtime=None): self._cont_inst = cont_inst + if not runtime: + runtime = settings.CONTAINER.runtime + self.runtime = runtime def run(self, command, demux=True, **kwargs): """Container approximation of Session.run.""" @@ -77,7 +80,7 @@ def run(self, command, demux=True, **kwargs): command = f"/bin/bash -c '{command}'" result = self._cont_inst._cont_inst.exec_run(command, **kwargs) if demux: - result = helpers.Result.from_duplexed_exec(result) + result = helpers.Result.from_duplexed_exec(result, self.runtime) else: result = helpers.Result.from_nonduplexed_exec(result) return result diff --git a/broker_settings.yaml.example b/broker_settings.yaml.example index 858c4b6a..6c41583b 100644 --- a/broker_settings.yaml.example +++ b/broker_settings.yaml.example @@ -34,12 +34,13 @@ Container: host_username: "" host_password: "" host_port: None + runtime: docker default: True - remote: host: "" host_username: "" host_password: "" - runtime: 'docker' + runtime: podman # name used to prefix container names, used to distinguish yourself # if not set, then your local username will be used # name_prefix: test diff --git a/tests/data/cli_scenarios/containers/execute_ch-d_ubi8.yaml b/tests/data/cli_scenarios/containers/execute_ch-d_ubi8.yaml index 24e0bda3..4ceb5a59 100644 --- a/tests/data/cli_scenarios/containers/execute_ch-d_ubi8.yaml +++ b/tests/data/cli_scenarios/containers/execute_ch-d_ubi8.yaml @@ -1,2 +1,2 @@ container_app: ubi8:latest -command: "ls -lah" +command: pwd diff --git a/tests/functional/README.md b/tests/functional/README.md index 4e48a9c2..55238c8c 100644 --- a/tests/functional/README.md +++ b/tests/functional/README.md @@ -8,7 +8,7 @@ Do not attempt to use Broker while running these functional tests or you may end Setup: - Ensure either Docker or Podman are installed and configured either locally or on a remote host. - Ensure Broker's Container provider is configured with the details of the previous step. -- Clone the [content-host-d](https://github.com/JacobCallahan/content-host-d) repository and build the UBI8 image, tagging it as `ch-d:ubi8`. +- Clone the [content-host-d](https://github.com/JacobCallahan/content-host-d) repository and build the UBI[7-9] images, tagging them as `ubi[7-9]` respectively. **SatLab Tests** diff --git a/tests/functional/test_containers.py b/tests/functional/test_containers.py index 4373863d..2e91dca7 100644 --- a/tests/functional/test_containers.py +++ b/tests/functional/test_containers.py @@ -125,9 +125,9 @@ def test_container_e2e_mp(): def test_broker_multi_manager(): with Broker.multi_manager( - ubi7={"container_host": "ubi7:latest"}, - ubi8={"container_host": "ubi8:latest", "_count": 2}, - ubi9={"container_host": "ubi9:latest"}, + ubi7={"container_host": "localhost/ubi7:latest"}, + ubi8={"container_host": "localhost/ubi8:latest", "_count": 2}, + ubi9={"container_host": "localhost/ubi9:latest"}, ) as multi_hosts: assert "ubi7" in multi_hosts assert "ubi8" in multi_hosts