fix: render per-instance container image from the verified manifest (${PIC_IMAGE})
Unit Tests / test (push) Successful in 9m54s

Connectivity compose-templates hardcoded an unpinned image:tag (proxy even
referenced the renamed-away svc-redsocks), so the per-instance container that
actually ran pulled an unverified :latest — bypassing the cosign/digest
verification the store performs at install. Add a ${PIC_IMAGE} render variable
that resolves to the manifest's digest-pinned, verified image; the matching
pic-services templates switch to image: ${PIC_IMAGE} so the container that runs
is exactly the ref the store verified.

Verified on pic1: rendering the proxy template yields the pinned
svc-proxy@sha256 image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 11:02:04 -04:00
parent 6bc1d625bf
commit c806a9bb54
2 changed files with 22 additions and 0 deletions
+5
View File
@@ -157,6 +157,11 @@ class ServiceComposer:
result = result.replace('${PIC_CELL_NAME}', cell_name)
result = result.replace('${PIC_SERVICE_ID}', service_id)
result = result.replace('${PIC_DATA_DIR}', str(Path(self.data_dir).resolve()))
# ${PIC_IMAGE} resolves to the manifest's image — the digest-pinned,
# cosign-verified reference. Templates (especially instanceable ones)
# MUST use this rather than hardcoding an image:tag, so the container
# that actually runs is the same image the store verified at install.
result = result.replace('${PIC_IMAGE}', str(manifest.get('image', '')))
if instance_vars:
for var in ('INSTANCE_ID', 'REDIRECT_PORT'):
+17
View File
@@ -88,6 +88,23 @@ class TestRenderTemplate(unittest.TestCase):
result = self.composer.render_template('myservice', manifest, template)
self.assertEqual(result, 'ID=myservice')
def test_pic_image_substituted_from_manifest(self):
# ${PIC_IMAGE} must resolve to the manifest's digest-pinned image so the
# per-instance container runs the exact ref the store verified — not an
# unpinned :latest hardcoded in the template.
digest = 'git.pic.ngo/roof/svc-proxy:latest@sha256:' + 'a' * 64
manifest = _make_manifest()
manifest['image'] = digest
template = 'image: ${PIC_IMAGE}'
result = self.composer.render_template('myservice', manifest, template)
self.assertEqual(result, f'image: {digest}')
def test_pic_image_empty_when_manifest_has_no_image(self):
manifest = _make_manifest()
result = self.composer.render_template(
'myservice', manifest, 'image: ${PIC_IMAGE}')
self.assertEqual(result, 'image: ')
def test_pic_secret_generated_and_substituted(self):
with tempfile.TemporaryDirectory() as tmpdir:
composer = _composer(data_dir=tmpdir)