fix: unblock instanceable connectivity store-service install + clean up on delete

Live verification on pic1 of the connectivity v2 multi-instance feature
surfaced four integration bugs that prevented installing any published
connectivity store service (proxy/wireguard-ext/openvpn-client/sshuttle)
and left stale host routing state behind. All four are fixed here:

1. manifest_validator rejected the CI-published `name:tag@sha256:<digest>`
   image form (it required digest-only), while service_store_manager already
   accepted it — so every published store image failed validation. Allow an
   optional tag before the digest, matching service_store_manager.

2. The cell-api image shipped the docker CLI but not the Compose v2 plugin,
   so every `docker compose` ServiceComposer runs (pull/up/down for store
   services) failed with "'compose' is not a docker command". Copy the
   compose plugin binary from the docker-cli stage.

3. service_store_manager.install ran the base compose up for instanceable
   services, whose template still contains ${INSTANCE_ID}/${REDIRECT_PORT}
   (there is no base container — one runs per connection instance). It now
   verifies the image signature but defers the container to connection
   creation for instanceable manifests.

4. delete_connection freed the record/secrets/container but never removed the
   connection's individually-managed `ip rule fwmark->table` or its FORWARD
   kill-switch (apply_routes only flushes the PIC_CONNECTIVITY chains and
   re-adds rules for surviving connections), leaking stale host routing state.
   It now tears both down; added _remove_killswitch.

Verified end-to-end on pic1: two proxy instances allocate distinct
marks/tables/ports (skipping in-use resources), render distinct per-instance
containers, two peers route through distinct instances (per-peer MARK +
REDIRECT), delete is blocked while referenced (409) and cleans its ip rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 08:45:32 -04:00
parent 4b3d695805
commit 6bc1d625bf
7 changed files with 160 additions and 10 deletions
+37
View File
@@ -726,6 +726,43 @@ class TestInstall(unittest.TestCase):
self.assertIn('digest', result['error'].lower())
composer.install.assert_not_called()
def test_install_instanceable_verifies_image_but_does_not_up_container(self):
"""Instanceable services defer the container to connection creation.
Their base compose template still contains ${INSTANCE_ID}/${REDIRECT_PORT},
so the base container must NOT be rendered/pulled/up'd at install time —
only the image signature is verified, and the record (with the raw
template) is stored for ConnectivityManager to render per instance.
"""
manifest = _valid_manifest(
id='proxy', container_name='cell-proxy-${INSTANCE_ID}',
instanceable=True,
)
ssm, cm, _, composer = _make_ssm(manifest=manifest)
cm.get_image_verification_mode.return_value = 'enforce'
composer.verify_image.return_value = {'ok': True}
result = ssm.install('proxy')
self.assertTrue(result['ok'], result)
composer.verify_image.assert_called_once()
composer.install.assert_not_called()
# The raw template is persisted so per-instance rendering needs no refetch.
record = cm.set_installed_service.call_args[0][1]
self.assertIn('compose_template', record)
def test_install_instanceable_aborts_when_image_verification_fails(self):
"""An instanceable service whose image fails verification must not install."""
manifest = _valid_manifest(
id='proxy', container_name='cell-proxy-${INSTANCE_ID}',
instanceable=True,
)
ssm, cm, _, composer = _make_ssm(manifest=manifest)
cm.get_image_verification_mode.return_value = 'enforce'
composer.verify_image.return_value = {'ok': False, 'error': 'signature verification failed'}
result = ssm.install('proxy')
self.assertFalse(result['ok'])
composer.install.assert_not_called()
cm.set_installed_service.assert_not_called()
def test_install_without_composer_stores_record(self):
"""When service_composer=None, skip compose but still store the install record."""
manifest = _valid_manifest(id='myapp', container_name='cell-myapp')