Fix Docker client installation on Debian 13
This commit is contained in:
+13
-4
@@ -191,15 +191,24 @@ if len(versions) != 1 or versions[0] != manifest.get('version') or not re.fullma
|
|||||||
compile(agent, 'ec-agent.py', 'exec')
|
compile(agent, 'ec-agent.py', 'exec')
|
||||||
compile(updater, 'ec-agent-update.py', 'exec')
|
compile(updater, 'ec-agent-update.py', 'exec')
|
||||||
PY
|
PY
|
||||||
|
# INSTALLATION_DOCKER: Debian 13 splits the client out of docker.io.
|
||||||
if ! command -v docker >/dev/null; then
|
if ! command -v docker >/dev/null; then
|
||||||
if [ -e /var/lib/docker ] || [ -e /etc/docker/daemon.json ]; then
|
if [ "$VERSION_ID" = 13 ] && [ "$(dpkg-query -W -f='${Status}' docker.io 2>/dev/null)" = 'install ok installed' ]; then
|
||||||
|
# Recover an earlier daemon-only install, even if it created Docker data.
|
||||||
|
# Add only the client; do not replace or restart the existing engine.
|
||||||
|
apt-get install -y --no-install-recommends --no-remove docker-cli
|
||||||
|
elif [ -e /var/lib/docker ] || [ -e /etc/docker/daemon.json ]; then
|
||||||
echo 'Existing Docker data/configuration detected without a Docker CLI. Repair Docker, then rerun.' >&2
|
echo 'Existing Docker data/configuration detected without a Docker CLI. Repair Docker, then rerun.' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
else
|
||||||
apt-get install -y --no-install-recommends docker.io
|
docker_packages=(docker.io)
|
||||||
|
if [ "$VERSION_ID" = 13 ]; then docker_packages+=(docker-cli); fi
|
||||||
|
apt-get install -y --no-install-recommends "${docker_packages[@]}"
|
||||||
systemctl enable --now docker
|
systemctl enable --now docker
|
||||||
fi
|
fi
|
||||||
docker info >/dev/null 2>&1 || { echo 'Docker is installed but unavailable. Start or repair it, then rerun.' >&2; exit 1; }
|
fi
|
||||||
|
command -v docker >/dev/null || { echo 'Docker CLI is missing after installation. Repair the Docker client package, then rerun.' >&2; exit 1; }
|
||||||
|
docker info >/dev/null || { echo 'Docker is installed but unavailable. Inspect systemctl status docker and journalctl -u docker, then rerun.' >&2; exit 1; }
|
||||||
install -d -m 0700 /etc/electric-cherry
|
install -d -m 0700 /etc/electric-cherry
|
||||||
# Wait for an already-running updater to finish; never interrupt its rollback.
|
# Wait for an already-running updater to finish; never interrupt its rollback.
|
||||||
# The same lock also prevents a timer firing while files are being installed.
|
# The same lock also prevents a timer firing while files are being installed.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ PREFLIGHT = next(block for block in BLOCKS if "# INSTALLATION_PREFLIGHT:" in blo
|
|||||||
OWNERSHIP = next(block for block in BLOCKS if "# INSTALLATION_OWNERSHIP:" in block)
|
OWNERSHIP = next(block for block in BLOCKS if "# INSTALLATION_OWNERSHIP:" in block)
|
||||||
RELEASE = next(block for block in BLOCKS if "# INSTALLATION_RELEASE:" in block)
|
RELEASE = next(block for block in BLOCKS if "# INSTALLATION_RELEASE:" in block)
|
||||||
CLEANUP = re.search(r"installation_cleanup\(\) \{.*?\n\}", SOURCE, re.DOTALL).group()
|
CLEANUP = re.search(r"installation_cleanup\(\) \{.*?\n\}", SOURCE, re.DOTALL).group()
|
||||||
|
DOCKER = SOURCE[SOURCE.index("# INSTALLATION_DOCKER:"):SOURCE.index("install -d -m 0700 /etc/electric-cherry")]
|
||||||
SPEC = importlib.util.spec_from_file_location("package_release", Path(__file__).resolve().parents[1] / "package_release.py")
|
SPEC = importlib.util.spec_from_file_location("package_release", Path(__file__).resolve().parents[1] / "package_release.py")
|
||||||
PACKAGER = importlib.util.module_from_spec(SPEC)
|
PACKAGER = importlib.util.module_from_spec(SPEC)
|
||||||
SPEC.loader.exec_module(PACKAGER)
|
SPEC.loader.exec_module(PACKAGER)
|
||||||
@@ -230,6 +231,109 @@ class InstallerTests(unittest.TestCase):
|
|||||||
self.preflight()
|
self.preflight()
|
||||||
|
|
||||||
|
|
||||||
|
class DockerInstallerTests(unittest.TestCase):
|
||||||
|
def run_docker_setup(self, version="13", cli=False, engine=False, available=True,
|
||||||
|
data=False, config=False, apt_status=0, supply_cli=True):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
base = Path(temporary)
|
||||||
|
docker_data = base / "docker-data"
|
||||||
|
docker_config = base / "daemon.json"
|
||||||
|
if data:
|
||||||
|
docker_data.mkdir()
|
||||||
|
(docker_data / "sentinel").write_text("existing containers")
|
||||||
|
if config:
|
||||||
|
docker_config.write_text("existing configuration")
|
||||||
|
block = DOCKER.replace("/var/lib/docker", str(docker_data)).replace("/etc/docker/daemon.json", str(docker_config))
|
||||||
|
# Simulate Debian's package split and Docker state. All commands that
|
||||||
|
# could install packages or touch services are shell stubs.
|
||||||
|
script = r'''
|
||||||
|
set -euo pipefail
|
||||||
|
command() { [ "$*" = '-v docker' ] && [ "$EC_TEST_CLI" = true ]; }
|
||||||
|
dpkg-query() {
|
||||||
|
[ "$*" = '-W -f=${Status} docker.io' ] || exit 90
|
||||||
|
[ "$EC_TEST_ENGINE" = true ] || return 1
|
||||||
|
printf 'install ok installed'
|
||||||
|
}
|
||||||
|
apt-get() {
|
||||||
|
printf 'apt-get %s\n' "$*"
|
||||||
|
[ "$EC_TEST_APT_STATUS" = 0 ] || return "$EC_TEST_APT_STATUS"
|
||||||
|
if [ "$EC_TEST_SUPPLY_CLI" = true ]; then
|
||||||
|
case " $* " in
|
||||||
|
*' docker-cli '*) EC_TEST_CLI=true ;;
|
||||||
|
*' docker.io '*) if [ "$VERSION_ID" = 12 ]; then EC_TEST_CLI=true; fi ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
systemctl() { printf 'systemctl %s\n' "$*"; }
|
||||||
|
docker() {
|
||||||
|
[ "$EC_TEST_CLI" = true ] || return 127
|
||||||
|
[ "$*" = info ] || exit 91
|
||||||
|
if [ "$EC_TEST_AVAILABLE" != true ]; then
|
||||||
|
echo 'Cannot connect to the Docker daemon' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
environment = {**os.environ, "VERSION_ID": version,
|
||||||
|
"EC_TEST_CLI": str(cli).lower(), "EC_TEST_ENGINE": str(engine).lower(),
|
||||||
|
"EC_TEST_AVAILABLE": str(available).lower(), "EC_TEST_APT_STATUS": str(apt_status),
|
||||||
|
"EC_TEST_SUPPLY_CLI": str(supply_cli).lower()}
|
||||||
|
result = subprocess.run(["bash", "-c", script + block], env=environment, capture_output=True, text=True)
|
||||||
|
if data:
|
||||||
|
self.assertEqual((docker_data / "sentinel").read_text(), "existing containers")
|
||||||
|
if config:
|
||||||
|
self.assertEqual(docker_config.read_text(), "existing configuration")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test_fresh_debian_12_and_13_install_both_engine_and_client(self):
|
||||||
|
for version, packages in (("12", "docker.io"), ("13", "docker.io docker-cli")):
|
||||||
|
with self.subTest(version=version):
|
||||||
|
result = self.run_docker_setup(version=version)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stdout.splitlines(), [
|
||||||
|
f"apt-get install -y --no-install-recommends {packages}",
|
||||||
|
"systemctl enable --now docker"])
|
||||||
|
|
||||||
|
def test_rerun_repairs_debian_13_missing_client_without_touching_engine(self):
|
||||||
|
result = self.run_docker_setup(engine=True, data=True, config=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stdout.splitlines(), [
|
||||||
|
"apt-get install -y --no-install-recommends --no-remove docker-cli"])
|
||||||
|
|
||||||
|
def test_existing_docker_including_ce_is_reused_without_package_or_service_changes(self):
|
||||||
|
for engine in (False, True):
|
||||||
|
with self.subTest(debian_engine=engine):
|
||||||
|
result = self.run_docker_setup(cli=True, engine=engine, data=True, config=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stdout, "")
|
||||||
|
|
||||||
|
def test_unknown_existing_data_or_configuration_still_blocks_engine_installation(self):
|
||||||
|
for data, config in ((True, False), (False, True)):
|
||||||
|
with self.subTest(data=data, config=config):
|
||||||
|
result = self.run_docker_setup(data=data, config=config)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("Existing Docker data/configuration", result.stderr)
|
||||||
|
self.assertEqual(result.stdout, "")
|
||||||
|
|
||||||
|
def test_unavailable_existing_engine_exposes_docker_error_without_restarting_it(self):
|
||||||
|
result = self.run_docker_setup(cli=True, available=False)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("Cannot connect to the Docker daemon", result.stderr)
|
||||||
|
self.assertIn("journalctl -u docker", result.stderr)
|
||||||
|
self.assertEqual(result.stdout, "")
|
||||||
|
|
||||||
|
def test_package_failure_stops_before_starting_docker(self):
|
||||||
|
result = self.run_docker_setup(apt_status=42)
|
||||||
|
self.assertEqual(result.returncode, 42)
|
||||||
|
self.assertNotIn("systemctl", result.stdout)
|
||||||
|
|
||||||
|
def test_missing_client_is_distinguished_from_an_unavailable_daemon(self):
|
||||||
|
result = self.run_docker_setup(supply_cli=False)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("Docker CLI is missing", result.stderr)
|
||||||
|
self.assertNotIn("Docker is installed but unavailable", result.stderr)
|
||||||
|
|
||||||
|
|
||||||
class ReleaseTests(unittest.TestCase):
|
class ReleaseTests(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.temporary = tempfile.TemporaryDirectory()
|
self.temporary = tempfile.TemporaryDirectory()
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ the agent owns local execution and its durable operation journal.
|
|||||||
A fresh destination host downloads centrally stored game files without manual
|
A fresh destination host downloads centrally stored game files without manual
|
||||||
copying or per-container setup. EC does not supply the game content itself.
|
copying or per-container setup. EC does not supply the game content itself.
|
||||||
|
|
||||||
|
The installer uses Debian's Docker packages: `docker.io` on Debian 12, plus
|
||||||
|
`docker-cli` on Debian 13. Rerunning it after an earlier Debian 13 install that
|
||||||
|
omitted the client adds the missing client without restarting the existing engine.
|
||||||
|
An already available Docker installation (including Docker CE) is reused. If the
|
||||||
|
daemon is unavailable, the installer reports the Docker error and stops before
|
||||||
|
enrollment; inspect `systemctl status docker` and `journalctl -u docker`.
|
||||||
|
|
||||||
The controller reconciles automatic setup on agent heartbeats and operation
|
The controller reconciles automatic setup on agent heartbeats and operation
|
||||||
polls. It first pulls `PLUTAINER_IMAGE` (default
|
polls. It first pulls `PLUTAINER_IMAGE` (default
|
||||||
`ghcr.io/ayymoss/plutainer:latest`), then prepares the latest verified upload for
|
`ghcr.io/ayymoss/plutainer:latest`), then prepares the latest verified upload for
|
||||||
|
|||||||
+13
-4
@@ -191,15 +191,24 @@ if len(versions) != 1 or versions[0] != manifest.get('version') or not re.fullma
|
|||||||
compile(agent, 'ec-agent.py', 'exec')
|
compile(agent, 'ec-agent.py', 'exec')
|
||||||
compile(updater, 'ec-agent-update.py', 'exec')
|
compile(updater, 'ec-agent-update.py', 'exec')
|
||||||
PY
|
PY
|
||||||
|
# INSTALLATION_DOCKER: Debian 13 splits the client out of docker.io.
|
||||||
if ! command -v docker >/dev/null; then
|
if ! command -v docker >/dev/null; then
|
||||||
if [ -e /var/lib/docker ] || [ -e /etc/docker/daemon.json ]; then
|
if [ "$VERSION_ID" = 13 ] && [ "$(dpkg-query -W -f='${Status}' docker.io 2>/dev/null)" = 'install ok installed' ]; then
|
||||||
|
# Recover an earlier daemon-only install, even if it created Docker data.
|
||||||
|
# Add only the client; do not replace or restart the existing engine.
|
||||||
|
apt-get install -y --no-install-recommends --no-remove docker-cli
|
||||||
|
elif [ -e /var/lib/docker ] || [ -e /etc/docker/daemon.json ]; then
|
||||||
echo 'Existing Docker data/configuration detected without a Docker CLI. Repair Docker, then rerun.' >&2
|
echo 'Existing Docker data/configuration detected without a Docker CLI. Repair Docker, then rerun.' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
else
|
||||||
apt-get install -y --no-install-recommends docker.io
|
docker_packages=(docker.io)
|
||||||
|
if [ "$VERSION_ID" = 13 ]; then docker_packages+=(docker-cli); fi
|
||||||
|
apt-get install -y --no-install-recommends "${docker_packages[@]}"
|
||||||
systemctl enable --now docker
|
systemctl enable --now docker
|
||||||
fi
|
fi
|
||||||
docker info >/dev/null 2>&1 || { echo 'Docker is installed but unavailable. Start or repair it, then rerun.' >&2; exit 1; }
|
fi
|
||||||
|
command -v docker >/dev/null || { echo 'Docker CLI is missing after installation. Repair the Docker client package, then rerun.' >&2; exit 1; }
|
||||||
|
docker info >/dev/null || { echo 'Docker is installed but unavailable. Inspect systemctl status docker and journalctl -u docker, then rerun.' >&2; exit 1; }
|
||||||
install -d -m 0700 /etc/electric-cherry
|
install -d -m 0700 /etc/electric-cherry
|
||||||
# Wait for an already-running updater to finish; never interrupt its rollback.
|
# Wait for an already-running updater to finish; never interrupt its rollback.
|
||||||
# The same lock also prevents a timer firing while files are being installed.
|
# The same lock also prevents a timer firing while files are being installed.
|
||||||
|
|||||||
Reference in New Issue
Block a user