Skip to content

Commit

Permalink
Broker changes for remote podman (tcp and ipv6)
Browse files Browse the repository at this point in the history
We have encountered a couple of issues that need to be solved in order to use Broker with remote podman hosts.

  First is an issue with the ssh\-based connection to remote podman that was investigated in a previous issue. The
  solution to this from Broker's side is largely changing the connection string based on the host\_port given.

  Second is to give Broker the ability to automatically find and pass along the correct network information when
  spinning up an ipv6 container. The solution for this is to list networks, search for one that is ipv6 enabled, then
  modify the container creation arguments to pass along that network information
  • Loading branch information
JacobCallahan committed Aug 5, 2024
1 parent 60a5294 commit 414f942
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 5 deletions.
26 changes: 24 additions & 2 deletions broker/binds/containers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""A collection of classes to ease interaction with Docker and Podman libraries."""
from broker.exceptions import UserError
from broker.settings import settings

HEADER_SIZE = 8
STDOUT = 1
STDERR = 2
SSH_PORT = 22


def demux_output(data_bytes):
Expand Down Expand Up @@ -79,6 +82,11 @@ def containers(self):
"""Return a list of containers on the container host."""
return self.client.containers.list(all=True)

@property
def networks(self):
"""Return a list of networks on the container host."""
return self.client.networks.list()

def image_info(self, name):
"""Return curated information about an image on the container host."""
if image := self.client.images.get(name):
Expand All @@ -91,6 +99,12 @@ def image_info(self, name):

def create_container(self, image, command=None, **kwargs):
"""Create and return running container instance."""
if net_name := settings.container.network:
if not self.get_network_by_attrs({"name": net_name}):
raise UserError(
f"Network '{settings.container.network}' not found on container host."
)
kwargs["networks"] = {net_name: {"NetworkId": net_name}}
kwargs = self._sanitize_create_args(kwargs)
return self.client.containers.create(image, command, **kwargs)

Expand All @@ -107,6 +121,12 @@ def pull_image(self, name):
"""Pull an image into the container host."""
return self.client.images.pull(name)

def get_network_by_attrs(self, attr_dict):
"""Return the first matching network that matches all attr_dict keys and values."""
for network in self.networks:
if all(network.attrs.get(k) == v for k, v in attr_dict.items()):
return network

@staticmethod
def get_logs(container):
"""Return the logs from a container."""
Expand Down Expand Up @@ -144,8 +164,10 @@ def __init__(self, **kwargs):
self._ClientClass = PodmanClient
if self.host == "localhost":
self.uri = "unix:///run/user/1000/podman/podman.sock"
else:
elif kwargs.get("port") == SSH_PORT:
self.uri = "http+ssh://{username}@{host}:{port}/run/podman/podman.sock".format(**kwargs)
else:
self.uri = "tcp://{host}:{port}".format(**kwargs)

def _sanitize_create_args(self, kwargs):
from podman.domain.containers_create import CreateMixin
Expand Down Expand Up @@ -176,7 +198,7 @@ def __init__(self, port=2375, **kwargs):
if self.host == "localhost":
self.uri = "unix://var/run/docker.sock"
else:
self.uri = "ssh://{username}@{host}".format(**kwargs)
self.uri = "tcp://{username}@{host}".format(**kwargs)

def _sanitize_create_args(self, kwargs):
from docker.models.containers import RUN_CREATE_KWARGS, RUN_HOST_CONFIG_KWARGS
Expand Down
5 changes: 3 additions & 2 deletions broker/providers/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class Container(Provider):
Validator(
"CONTAINER.host_password",
),
Validator("CONTAINER.host_port", default=22),
Validator("CONTAINER.host_port", default=2375),
Validator("CONTAINER.network", default=None),
Validator("CONTAINER.timeout", default=360),
Validator("CONTAINER.auto_map_ports", is_type_of=bool, default=True),
]
Expand Down Expand Up @@ -213,7 +214,7 @@ def construct_host(self, provider_params, host_classes, **kwargs):
# add the container's port mapping to the host instance only if there are any ports open
if cont_attrs.get("ports"):
host_inst.exposed_ports = {
f"{k.split('/')[0]}": v[0]["HostPort"] for k, v in cont_attrs["ports"].items()
f"{k.split('/')[0]}": v[0]["HostPort"] for k, v in cont_attrs["ports"].items() if v
}
return host_inst

Expand Down
1 change: 1 addition & 0 deletions broker_settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Container:
host_password: "<plain text password>"
host_port: None
runtime: docker
network: null
default: True
- remote:
host: "<remote hostname>"
Expand Down
59 changes: 59 additions & 0 deletions tests/data/container/fake_networks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[
{
"name": "podman1",
"id": "78a8f74414",
"driver": "bridge",
"network_interface": "cni-podman1",
"created": "2024-04-29T04:39:08.09789015-04:00",
"subnets": [
{
"subnet": "fd00::1:8:0/112",
"gateway": "fd00::1:8:1"
}
],
"ipv6_enabled": true,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
},
{
"name": "podman2",
"id": "c785da3b9b",
"driver": "bridge",
"network_interface": "cni-podman2",
"created": "2024-04-29T04:32:25.30689015-04:00",
"subnets": [
{
"subnet": "10.89.0.0/24",
"gateway": "10.89.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
},
{
"name": "podman",
"id": "c0b7a1bb95",
"driver": "bridge",
"network_interface": "cni-podman0",
"created": "2024-07-26T15:05:00.256703131-04:00",
"subnets": [
{
"subnet": "10.88.0.0/16",
"gateway": "10.88.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
}
]
25 changes: 24 additions & 1 deletion tests/providers/test_container.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from pathlib import Path
import pytest
from broker.broker import Broker
from broker.providers.container import Container
from broker.helpers import MockStub
from broker.providers.container import Container
from broker.settings import settings


class ContainerApiStub(MockStub):
Expand Down Expand Up @@ -35,6 +37,16 @@ def __init__(self, **kwargs):
else:
super().__init__(in_dict=in_dict, **kwargs)

@property
def networks(self):
return [MockStub(obj) for obj in json.loads(Path("tests/data/container/fake_networks.json").read_text())]

def get_network_by_attrs(self, attr_dict):
"""Return the first matching network that matches all attr_dict keys and values."""
for network in self.networks:
if all(network.attrs.get(k) == v for k, v in attr_dict.items()):
return network

@staticmethod
def pull_image(tag_name):
with open("tests/data/container/fake_images.json") as image_file:
Expand All @@ -45,12 +57,17 @@ def pull_image(tag_name):
raise Broker.ProviderError(f"Unable to find image: {tag_name}")

def create_container(self, container_host, **kwargs):
if net_name := settings.container.network:
if not self.get_network_by_attrs({"name": net_name}):
raise Exception(f"Network '{settings.container.network}' not found on container host.")
kwargs["networks"] = {net_name: {"NetworkId": net_name}}
with open("tests/data/container/fake_containers.json") as container_file:
container_data = json.load(container_file)
image_data = self.pull_image(container_host)
for container in container_data:
if container["Config"]["Image"] == image_data.RepoTags[0]:
container["id"] = container["Id"] # hostname = cont_inst.id[:12]
container["kwargs"] = kwargs
return MockStub(container)


Expand All @@ -76,6 +93,12 @@ def test_host_creation(container_stub):
assert host.hostname == "f37d3058317f"


def test_ipv6_host_creation(container_stub):
settings.container.network = "podman2"
cont = container_stub.run_container(container_host="ch-d:ubi8")
assert cont.kwargs["networks"] == {'podman2': {'NetworkId': 'podman2'}}


def test_image_lookup_failure(container_stub):
with pytest.raises(Broker.ProviderError) as err:
container_stub.run_container(container_host="this-does-not-exist")
Expand Down

0 comments on commit 414f942

Please sign in to comment.