Commit 3f65eeb1 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Rework Docker inspection

This fixes a bug introduced by 8be88413
and hopefully makes Docker inspection a little easier to understand.

Changelog Note:
  `behave_util.docker`:
    - Adds `inspect()` which replaces `Item.inspect()` methods
    - Removes `Item` class
    > Note users will have to replace any method calls to the function.
parent a4e31883
Loading
Loading
Loading
Loading
+25 −25
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ from typing import Iterable
from typing import Iterator
from typing import MutableMapping
from typing import NewType
from typing import Protocol
from typing import Tuple
from typing import TypeVar
from typing import Union
@@ -103,6 +104,20 @@ def docker_quiet(*args: Argument, **env: str) -> None:
	run([DOCKER, *args], env={**environ, **env}, check=True, stdout=DEVNULL)


def inspect(item: DockerItem|str) -> JSONObject:
	"""
	Get the result of inspecting a Docker item instance, or string identifier
	"""
	if not isinstance(item, str):
		item = item.get_id()
	with Popen([DOCKER, 'inspect', item], stdout=PIPE) as proc:
		assert proc.stdout is not None
		results = json.load(proc.stdout)
	assert isinstance(results, list)
	assert len(results) == 1 and isinstance(results[0], dict)
	return JSONObject(results[0])


def _get_docker_host_ip() -> IPAddress:
	"""
	Return an IP address from the DOCKER_HOST environment variable, or a loopback address
@@ -148,33 +163,18 @@ class IPProtocol(Enum):
	UDP = 'udp'


class Item:
class DockerItem(Protocol):
	"""
	A mix-in for Docker items that can be inspected
	Protocol for all Docker item classes
	"""

	def __init__(self, reference: str):
		self.reference = reference

	def get_id(self) -> ShaID:
		"""
		Return an identifier for the Docker item
		"""
		return self.inspect().path("$.Id", str, ShaID)

	def inspect(self) -> JSONObject:
		"""
		Get the result of inspecting the Docker item
		"""
		with Popen([DOCKER, 'inspect', self.reference], stdout=PIPE) as proc:
			assert proc.stdout is not None
			results = json.load(proc.stdout)
		assert isinstance(results, list)
		assert len(results) == 1 and isinstance(results[0], dict)
		return JSONObject(results[0])


class Image(Item):
class Image:
	"""
	Docker image items
	"""
@@ -216,7 +216,7 @@ class Image(Item):

	@classmethod
	def _process_image(cls, reference: str) -> ShaID:
		report = Item(reference).inspect()
		report = inspect(reference)
		iid = report.path("$.Id", str, ShaID)
		cls._cache.update(
			((tag, iid) for tag in report.path("$.RepoTags", list[str])),
@@ -231,7 +231,7 @@ class Image(Item):
		return self.iid


class Container(Item):
class Container:
	"""
	Docker container items

@@ -296,7 +296,7 @@ class Container(Item):
		"""
		if self.cid is None:
			return False
		details = self.inspect()
		details = inspect(self)
		if details.path('$.State.Status', str) == 'exited':
			code = details.path('$.State.ExitCode', int)
			if code != 0:
@@ -367,7 +367,7 @@ class Container(Item):
		if self.is_running():
			return
		docker_quiet(b"container", b"start", self.get_id())
		assert self.inspect().path('$.State.Status', str) != 'created', \
		assert inspect(self).path('$.State.Status', str) != 'created', \
			"please report this at https://code.kodo.org.uk/dom/behave-utils/-/issues/11"

	def stop(self, rm: bool = False) -> None:
@@ -469,7 +469,7 @@ class Container(Item):
		Yield (address, port) combinations exposed on the host that map to the given container port
		"""
		name = f"{port}/{proto.name.lower()}"
		ports = self.inspect().path(
		ports = inspect(self).path(
			f"$.NetworkSettings.Ports.{name}",
			list[dict[str, str]],
		)
@@ -481,7 +481,7 @@ class Container(Item):
			yield (_get_docker_host_ip() if addr.is_unspecified else addr), port


class Network(Item):
class Network:
	"""
	A Docker network
	"""
@@ -600,7 +600,7 @@ class Network(Item):
		adding a container may invalidate the assurance that the address is free.
		"""
		# TODO: support IPv6
		data = self.inspect()
		data = inspect(self)
		# Considering only the first listed subnet
		net = data.path("$.IPAM.Config[0].Subnet", str, ipaddress.IPv4Network)

+3 −2
Original line number Diff line number Diff line
#  Copyright 2021, 2022  Dominik Sekotill <dom.sekotill@kodo.org.uk>
#  Copyright 2021-2023  Dominik Sekotill <dom.sekotill@kodo.org.uk>
#
#  This Source Code Form is subject to the terms of the Mozilla Public
#  License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -25,6 +25,7 @@ from .docker import Container
from .docker import Image
from .docker import Mount
from .docker import Network
from .docker import inspect
from .secret import make_secret
from .utils import wait

@@ -74,7 +75,7 @@ class Mysql(Container):
		"""
		Return a "host:port" string for connecting to the database from other containers
		"""
		host = self.inspect().path("$.Config.Hostname", str)
		host = inspect(self).path("$.Config.Hostname", str)
		return f"{host}:3306"

	@property