139 lines
6.4 KiB
Python
139 lines
6.4 KiB
Python
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from provisioning import docker
|
|
|
|
|
|
class Host:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.commands = []
|
|
self.installed = {}
|
|
self.partial = {}
|
|
self.plan = ''
|
|
self.path('/usr/sbin').mkdir(parents=True)
|
|
|
|
def path(self, path):
|
|
path = Path(path)
|
|
if str(path).startswith(str(self.root)):
|
|
return path
|
|
return self.root / str(path).lstrip('/')
|
|
|
|
def command(self, args, **kwargs):
|
|
self.commands.append(args)
|
|
code, output = 0, ''
|
|
if args[:2] == ['dpkg', '--print-architecture']:
|
|
output = 'amd64\n'
|
|
elif args[0] == 'dpkg-query':
|
|
output = ''.join(f'{name}\tinstall ok installed\t{version}\n' for name, version in self.installed.items())
|
|
output += ''.join(f'{name}\t{status}\t1.0\n' for name, status in self.partial.items())
|
|
elif args[0] == 'apt-get' and 'install' in args:
|
|
if '--simulate' in args:
|
|
output = self.plan
|
|
else:
|
|
for name in args[args.index('--no-remove') + 1:]:
|
|
self.installed[name] = '1.0'
|
|
self.partial.pop(name, None)
|
|
return subprocess.CompletedProcess(args, code, output, '')
|
|
|
|
|
|
class DockerPackageTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.host = Host(Path(self.temp.name).resolve())
|
|
for item in (
|
|
patch.object(docker, 'Path', self.host.path),
|
|
patch.object(docker, 'POLICY', self.host.path('/usr/sbin/policy-rc.d')),
|
|
patch.object(docker, 'POLICY_STATE', self.host.path('/usr/sbin/.apex-policy-rc.d')),
|
|
patch.object(docker.subprocess, 'run', side_effect=self.host.command),
|
|
):
|
|
item.start()
|
|
self.addCleanup(item.stop)
|
|
|
|
def test_package_database_drives_installation_and_repeat_does_not_upgrade(self):
|
|
self.host.installed['curl'] = '7.0'
|
|
self.host.partial['rsyslog'] = 'deinstall ok config-files'
|
|
self.assertEqual(docker.install_missing(['curl', 'rsyslog']), ['rsyslog'])
|
|
self.assertEqual(self.host.installed['curl'], '7.0')
|
|
self.assertEqual(self.host.installed['rsyslog'], '1.0')
|
|
count = len(self.host.commands)
|
|
self.assertEqual(docker.install_missing(['curl', 'rsyslog']), [])
|
|
self.assertFalse(any(command[0] == 'apt-get' for command in self.host.commands[count:]))
|
|
|
|
def test_dependency_upgrade_is_rejected_before_install(self):
|
|
self.host.plan = 'Inst libc6 [2.0] (3.0 Debian:13)\n'
|
|
with self.assertRaisesRegex(docker.DockerError, 'upgrade/removal'):
|
|
docker.install_missing(['git'])
|
|
self.assertNotIn('git', self.host.installed)
|
|
|
|
def test_held_installed_packages_are_preserved(self):
|
|
self.host.partial['git'] = 'hold ok installed'
|
|
self.assertEqual(docker.install_missing(['git']), [])
|
|
self.assertFalse(any(command[0] == 'apt-get' for command in self.host.commands))
|
|
|
|
def test_conflicting_docker_config_is_rejected_without_host_mutation(self):
|
|
path = self.host.path('/etc/docker/daemon.json')
|
|
path.parent.mkdir(parents=True)
|
|
path.write_text('{"data-root": "/data/docker"}')
|
|
before = sorted(str(path.relative_to(self.host.root)) for path in self.host.root.rglob('*'))
|
|
with self.assertRaisesRegex(docker.DockerError, 'Conflicting Docker'):
|
|
docker.docker_preflight(self.host.installed)
|
|
self.assertEqual(before, sorted(str(path.relative_to(self.host.root)) for path in self.host.root.rglob('*')))
|
|
self.assertFalse(any(command[0] in ('apt-get', 'systemctl') for command in self.host.commands))
|
|
|
|
def test_existing_docker_data_is_preserved(self):
|
|
data = self.host.path('/var/lib/docker/volumes/user-data')
|
|
data.parent.mkdir(parents=True)
|
|
data.write_text('valuable')
|
|
with self.assertRaisesRegex(docker.DockerError, 'Existing /var/lib/docker'):
|
|
docker.docker_preflight(self.host.installed)
|
|
self.assertEqual(data.read_text(), 'valuable')
|
|
|
|
def test_conflicting_distribution_docker_package_is_never_removed(self):
|
|
self.host.installed['docker.io'] = '26.1.5'
|
|
with self.assertRaisesRegex(docker.DockerError, 'Conflicting Docker packages'):
|
|
docker.docker_preflight(self.host.installed)
|
|
self.assertEqual(self.host.installed['docker.io'], '26.1.5')
|
|
self.assertFalse(any(command[0] == 'apt-get' for command in self.host.commands))
|
|
|
|
def test_unmanaged_destination_data_is_not_adopted(self):
|
|
data = self.host.path('/srv/docker/private-data')
|
|
data.parent.mkdir(parents=True)
|
|
data.write_text('preserve me')
|
|
with self.assertRaisesRegex(docker.DockerError, 'Unmanaged /srv/docker'):
|
|
docker.docker_preflight(self.host.installed)
|
|
self.assertEqual(data.read_text(), 'preserve me')
|
|
|
|
def test_service_policy_symlink_is_restored_after_failure(self):
|
|
policy = self.host.path('/usr/sbin/policy-rc.d')
|
|
original = self.host.path('/usr/sbin/provider-policy')
|
|
original.write_text('provider policy')
|
|
policy.symlink_to(original)
|
|
with self.assertRaisesRegex(RuntimeError, 'boom'):
|
|
with docker.service_start_guard():
|
|
self.assertFalse(policy.is_symlink())
|
|
raise RuntimeError('boom')
|
|
self.assertTrue(policy.is_symlink())
|
|
self.assertEqual(original.read_text(), 'provider policy')
|
|
|
|
def test_interrupted_guard_is_recovered_before_next_attempt(self):
|
|
state = self.host.path('/usr/sbin/.apex-policy-rc.d')
|
|
state.mkdir()
|
|
(state / 'original').write_text('original policy')
|
|
(state / 'ready').touch()
|
|
self.host.path('/usr/sbin/policy-rc.d').write_text('#!/bin/sh\nexit 101\n')
|
|
with docker.service_start_guard():
|
|
pass
|
|
self.assertEqual(self.host.path('/usr/sbin/policy-rc.d').read_text(), 'original policy')
|
|
|
|
def test_external_policy_change_is_not_overwritten(self):
|
|
policy = self.host.path('/usr/sbin/policy-rc.d')
|
|
with self.assertRaisesRegex(docker.DockerError, 'changed outside APEX'):
|
|
with docker.service_start_guard():
|
|
policy.write_text('external policy')
|
|
self.assertEqual(policy.read_text(), 'external policy')
|