diff --git a/api/service_composer.py b/api/service_composer.py index a85160d..dff548b 100644 --- a/api/service_composer.py +++ b/api/service_composer.py @@ -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'): diff --git a/tests/test_service_composer.py b/tests/test_service_composer.py index 77b92dd..d869c2e 100644 --- a/tests/test_service_composer.py +++ b/tests/test_service_composer.py @@ -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)