fix: harden news analysis and deploy verified image tags
This commit is contained in:
1 parent
3b7aec0024
commit
adb73f956b
27 files changed
+840
-147
No files matched your search
+2
-2
@@ -281,8 +281,8 @@ def publish():
|
||||
with open(output, 'a') as stream:
|
||||
stream.write(f'version={build["version"]}\ncommit={build["commit"]}\n')
|
||||
for part in IMAGES:
|
||||
stream.write(f'{part}={build["images"][part]["digest"]}\n')
|
||||
print('Verified development digests published', flush=True)
|
||||
stream.write(f'{part}={IMAGES[part]}:{build["version"]}\n')
|
||||
print('Verified development version tags published', flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+28
-11
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update the one data-bearing dev instance using verified image digests."""
|
||||
"""Update the one data-bearing dev instance using verified image version tags."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -12,7 +12,8 @@ import time
|
||||
|
||||
from ci import IMAGES, ROOT, branch_head, locked, require, run, save
|
||||
|
||||
SEARXNG_IMAGE = 'docker.io/searxng/searxng@sha256:f4aa660c54d062dedb268be84e821ea24e39f48c7a496af8dd02cc9eedb93847'
|
||||
SEARXNG_IMAGE = 'docker.io/searxng/searxng:2026.10.30-ffa88e3ba'
|
||||
SEARXNG_DIGEST = 'sha256:f4aa660c54d062dedb268be84e821ea24e39f48c7a496af8dd02cc9eedb93847'
|
||||
|
||||
INSTANCE = Path('/docker/gitea/dev/AlphaPulse')
|
||||
SERVICES = ('api', 'web', 'market', 'crawler', 'calendar', 'trader', 'news-risk', 'jin10-calendar', 'searxng')
|
||||
@@ -91,19 +92,28 @@ def validate_images(build):
|
||||
require(re.fullmatch(r'\d{8}-[0-9a-f]{8}', build['version']), 'Invalid development version')
|
||||
require(build['version'].endswith(build['commit'][:8]), 'Version does not identify this commit')
|
||||
for part, repository in IMAGES.items():
|
||||
image = build['images'][part]['digest']
|
||||
require(re.fullmatch(re.escape(repository) + r'@sha256:[0-9a-f]{64}', image), 'Expected a development image digest')
|
||||
digest = build['images'][part]['digest']
|
||||
require(re.fullmatch(re.escape(repository) + r'@sha256:[0-9a-f]{64}', digest), 'Invalid image identity in build receipt')
|
||||
image = repository + ':' + build['version']
|
||||
run('docker', 'pull', image)
|
||||
info = json.loads(run('docker', 'image', 'inspect', image))[0]
|
||||
require(digest in info.get('RepoDigests', []), 'Version tag differs from the accepted image')
|
||||
labels = info['Config'].get('Labels') or {}
|
||||
require(labels.get('org.opencontainers.image.revision') == build['commit'], 'Image revision mismatch')
|
||||
require(labels.get('org.opencontainers.image.version') == build['version'], 'Image version mismatch')
|
||||
require(info['Architecture'] == 'amd64' and info['Os'] == 'linux', 'Unexpected image architecture')
|
||||
|
||||
|
||||
def validate_search_image():
|
||||
info = json.loads(run('docker', 'image', 'inspect', SEARXNG_IMAGE))[0]
|
||||
require(any(ref.rsplit('@', 1)[-1] == SEARXNG_DIGEST for ref in info.get('RepoDigests', [])),
|
||||
'Search version tag differs from the accepted image')
|
||||
return info
|
||||
|
||||
|
||||
def compose(directory, release):
|
||||
images = directory / 'images.env'
|
||||
images.write_text(''.join(f'ALPHAPULSE_{part.upper()}_IMAGE={release["images"][part]["digest"]}\n' for part in IMAGES))
|
||||
images.write_text(''.join(f'ALPHAPULSE_{part.upper()}_IMAGE={IMAGES[part]}:{release["version"]}\n' for part in IMAGES))
|
||||
return ['docker', 'compose', '--project-name', 'alphapulse-dev', '--env-file', str(images),
|
||||
'-f', str(directory / 'compose.yml')]
|
||||
|
||||
@@ -121,10 +131,11 @@ def health(command, build, expected_services=SERVICES):
|
||||
'Service exited, paused or restarted')
|
||||
service = container['Config']['Labels']['com.docker.compose.service']
|
||||
if service == 'searxng':
|
||||
require(container['Config']['Image'] == SEARXNG_IMAGE, 'Unexpected search image')
|
||||
expected = json.loads(run('docker', 'image', 'inspect', SEARXNG_IMAGE))[0]['Id']
|
||||
require(container['Image'] == expected, 'Unexpected search image')
|
||||
continue
|
||||
part = 'web' if container['Name'] == '/AlphaPulse-dev' else 'backend'
|
||||
expected = json.loads(run('docker', 'image', 'inspect', build['images'][part]['digest']))[0]['Id']
|
||||
expected = json.loads(run('docker', 'image', 'inspect', IMAGES[part] + ':' + build['version']))[0]['Id']
|
||||
require(container['Image'] == expected, 'Running image differs from the accepted digest')
|
||||
payload = run('docker', 'exec', 'AlphaPulse-dev-api', 'python', '-c',
|
||||
'import json,urllib.request; print(json.load(urllib.request.urlopen("http://web/api/health"))["data"]["commit"])')
|
||||
@@ -148,9 +159,10 @@ def deploy(build, directory=INSTANCE):
|
||||
return
|
||||
validate_images(build)
|
||||
run('docker', 'pull', SEARXNG_IMAGE)
|
||||
validate_search_image()
|
||||
# Only this read-only schema guard receives the existing application environment.
|
||||
print(run('docker', 'run', '--rm', '-i', '--network', 'dev_default', '--env-file', directory / '.env',
|
||||
build['images']['backend']['digest'], 'python', '-', input=SCHEMA), flush=True)
|
||||
IMAGES['backend'] + ':' + build['version'], 'python', '-', input=SCHEMA), flush=True)
|
||||
previous_file = directory / 'deployment.json'
|
||||
previous = json.loads(previous_file.read_text()) if previous_file.exists() else None
|
||||
old_compose = (directory / 'compose.yml').read_text() if previous else None
|
||||
@@ -213,7 +225,7 @@ def deploy(build, directory=INSTANCE):
|
||||
if previous:
|
||||
restore(directory, pending)
|
||||
raise
|
||||
print('All application services now run the accepted development digests', flush=True)
|
||||
print('All application services now run the accepted development version tags', flush=True)
|
||||
|
||||
|
||||
def restore(directory, pending):
|
||||
@@ -230,7 +242,12 @@ def restore(directory, pending):
|
||||
print('Restored preparation state; existing services were retained', flush=True)
|
||||
return
|
||||
validate_images(previous)
|
||||
(directory / 'compose.yml').write_text(pending['previous_compose'])
|
||||
previous_compose = pending['previous_compose'].replace('docker.io/searxng/searxng@' + SEARXNG_DIGEST, SEARXNG_IMAGE)
|
||||
require(not re.search(r'^\s*image:.*@sha256:', previous_compose, re.M),
|
||||
'Previous Compose contains an unknown digest reference; verify its version tag before recovery')
|
||||
if re.search(r'^ searxng:$', previous_compose, re.M):
|
||||
validate_search_image()
|
||||
(directory / 'compose.yml').write_text(previous_compose)
|
||||
command = compose(directory, previous)
|
||||
run(*command, 'up', '-d', '--no-build', '--pull', 'never', '--remove-orphans', '--wait', '--wait-timeout', '120', *pending['previous_running'])
|
||||
previous_services = re.findall(r'^ ([a-z][a-z0-9-]*):$', pending['previous_compose'], re.M)
|
||||
@@ -248,7 +265,7 @@ def reset_news_ai(build, confirmed=False):
|
||||
with locked(INSTANCE / '.deploy.lock'):
|
||||
require(not (INSTANCE / 'deployment.pending.json').exists(), 'Recover the pending deployment first')
|
||||
command = ['docker', 'run', '--rm', '-i', '--network', 'dev_default', '--env-file', str(INSTANCE / '.env'),
|
||||
build['images']['backend']['digest'], 'python', '-']
|
||||
IMAGES['backend'] + ':' + build['version'], 'python', '-']
|
||||
run(*command, input=SCHEMA)
|
||||
if confirmed:
|
||||
current = json.loads((INSTANCE / 'deployment.json').read_text())
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import os
|
||||
import contextlib
|
||||
import json
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -17,12 +19,61 @@ import mirror
|
||||
|
||||
|
||||
class BuildTests(unittest.TestCase):
|
||||
def test_search_health_checks_image_content_across_reference_styles(self):
|
||||
container = {'Name': '/search', 'Image': 'accepted-local-image', 'RestartCount': 0,
|
||||
'Config': {'Image': 'historical@sha256:' + '1' * 64,
|
||||
'Labels': {'com.docker.compose.service': 'searxng'}},
|
||||
'State': {'Running': True, 'Paused': False}}
|
||||
with patch.object(deploy, 'inspect_services', return_value=[container]), \
|
||||
patch.object(deploy, 'run', side_effect=[json.dumps([{'Id': 'accepted-local-image'}]), 'commit', 'commit', '200']):
|
||||
deploy.health([], {'commit': 'commit'}, ('searxng',))
|
||||
with patch.object(deploy, 'inspect_services', return_value=[container]), \
|
||||
patch.object(deploy, 'run', return_value=json.dumps([{'Id': 'different-local-image'}])):
|
||||
with self.assertRaisesRegex(RuntimeError, 'Unexpected search image'):
|
||||
deploy.health([], {'commit': 'commit'}, ('searxng',))
|
||||
|
||||
def test_version_is_utc_commit_identity(self):
|
||||
timestamp = datetime(2026, 10, 29, 23, 59, tzinfo=UTC).timestamp()
|
||||
self.assertEqual(ci.version('abcdef0123456789' * 2 + 'abcdef01', timestamp), '20261029-abcdef01')
|
||||
with self.assertRaises(RuntimeError):
|
||||
ci.version('abcdef0', timestamp)
|
||||
|
||||
def test_published_outputs_use_tags_and_receipt_keeps_digests(self):
|
||||
build = {'verified': True, 'commit': 'a'*40, 'version': '20261101-aaaaaaaa',
|
||||
'images': {part: {} for part in ci.IMAGES}}
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory=Path(temporary)
|
||||
(directory/'build.json').write_text(json.dumps(build))
|
||||
output=directory/'output'
|
||||
digests=[repository+'@sha256:'+'1'*64 for repository in ci.IMAGES.values()]
|
||||
with patch.object(ci,'ROOT',directory), patch.object(ci,'locked',return_value=contextlib.nullcontext()), \
|
||||
patch.object(ci,'existing_image',side_effect=digests), \
|
||||
patch.dict(os.environ,{'GITEA_EVENT_NAME':'push','GITEA_REF':'refs/heads/dev',
|
||||
'GITEA_SHA':build['commit'],'GITHUB_OUTPUT':str(output)}):
|
||||
ci.publish()
|
||||
published=json.loads((directory/'build.json').read_text())
|
||||
for (part,repository),digest in zip(ci.IMAGES.items(),digests):
|
||||
self.assertEqual(published['images'][part]['digest'],digest)
|
||||
self.assertIn(part+'='+repository+':'+build['version'],output.read_text())
|
||||
self.assertNotIn('@sha256:',output.read_text())
|
||||
|
||||
def test_version_tag_is_checked_against_accepted_digest(self):
|
||||
build={'verified':True,'commit':'a'*40,'version':'20261101-aaaaaaaa',
|
||||
'images':{part:{'digest':repository+'@sha256:'+'1'*64} for part,repository in ci.IMAGES.items()}}
|
||||
def docker(*args):
|
||||
if args[:2]==('docker','pull'): return ''
|
||||
part=next(p for p,r in ci.IMAGES.items() if args[-1].startswith(r+':'))
|
||||
return json.dumps([{'RepoDigests':[build['images'][part]['digest']],
|
||||
'Architecture':'amd64','Os':'linux','Config':{'Labels':{
|
||||
'org.opencontainers.image.revision':build['commit'],
|
||||
'org.opencontainers.image.version':build['version']}}}])
|
||||
with patch.object(deploy,'run',side_effect=docker) as run:
|
||||
deploy.validate_images(build)
|
||||
self.assertTrue(all('@sha256:' not in call.args[-1] for call in run.call_args_list))
|
||||
with patch.object(deploy,'run',return_value=json.dumps([{'RepoDigests':[]}])):
|
||||
with self.assertRaisesRegex(RuntimeError,'differs from the accepted image'):
|
||||
deploy.validate_images(build)
|
||||
|
||||
def test_existing_version_rejects_different_full_commit(self):
|
||||
commit = 'a' * 40
|
||||
manifest = {'config': {'digest': 'sha256:' + '0' * 64}}
|
||||
@@ -85,7 +136,7 @@ class NewsResetTests(unittest.TestCase):
|
||||
def test_preview_never_stops_worker_or_confirms_deletion(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
build = {'images': {'backend': {'digest': 'synthetic'}}}
|
||||
build = {'version':'20261101-aaaaaaaa', 'images': {'backend': {'digest': 'synthetic'}}}
|
||||
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'validate_images'), patch.object(deploy, 'run', return_value='{}') as run:
|
||||
deploy.reset_news_ai(build)
|
||||
self.assertEqual(run.call_count, 2)
|
||||
@@ -108,9 +159,9 @@ class RecoveryTests(unittest.TestCase):
|
||||
directory = Path(temporary)
|
||||
ci.save(directory / 'deployment.pending.json', {})
|
||||
services = [name for name in deploy.SERVICES if name != 'jin10-calendar']
|
||||
previous = {'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
previous = {'version':'20261101-aaaaaaaa', 'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
compose = 'services:\n' + ''.join(f' {name}:\n image: synthetic\n' for name in services)
|
||||
with patch.object(deploy, 'run', return_value='') as run, \
|
||||
with patch.object(deploy, 'run', return_value='') as run, patch.object(deploy, 'validate_search_image'), \
|
||||
patch.object(deploy, 'validate_images'), patch.object(deploy, 'health') as health:
|
||||
deploy.restore(directory, {'previous': previous, 'previous_compose': compose, 'previous_running': services})
|
||||
update = next(call.args for call in run.call_args_list if 'up' in call.args)
|
||||
@@ -122,13 +173,13 @@ class RecoveryTests(unittest.TestCase):
|
||||
def test_pending_write_failure_never_pauses_trader(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
previous = {'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
previous = {'version':'20261101-aaaaaaaa', 'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
ci.save(directory / 'deployment.json', previous)
|
||||
(directory / 'compose.yml').write_text('synthetic')
|
||||
running = [{'Config': {'Labels': {'com.docker.compose.service': 'trader'}}, 'State': {'Running': True}}]
|
||||
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'branch_head', return_value='a' * 40), \
|
||||
patch.object(deploy, 'validate_images'), patch.object(deploy, 'inspect_services', return_value=running), \
|
||||
patch.object(deploy, 'run', return_value='{"busy":0}') as run, \
|
||||
patch.object(deploy, 'run', return_value='{"busy":0}') as run, patch.object(deploy, 'validate_search_image'), \
|
||||
patch.object(deploy, 'save', side_effect=OSError('disk failure')):
|
||||
with self.assertRaises(OSError):
|
||||
deploy.deploy({**previous, 'commit': 'a' * 40}, directory)
|
||||
@@ -137,7 +188,7 @@ class RecoveryTests(unittest.TestCase):
|
||||
def test_busy_recheck_recovers_without_stopping_services(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
previous = {'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
previous = {'version':'20261101-aaaaaaaa', 'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
ci.save(directory / 'deployment.json', previous)
|
||||
(directory / 'compose.yml').write_text('synthetic')
|
||||
running = [{'Config': {'Labels': {'com.docker.compose.service': 'trader'}}, 'State': {'Running': True}}]
|
||||
@@ -149,6 +200,8 @@ class RecoveryTests(unittest.TestCase):
|
||||
return '[{"State":{"Paused":true}}]'
|
||||
if args[:2] == ('docker', 'ps'):
|
||||
return 'trader-id'
|
||||
if args[:3] == ('docker', 'image', 'inspect'):
|
||||
return json.dumps([{'RepoDigests': ['searxng/searxng@' + deploy.SEARXNG_DIGEST]}])
|
||||
return '{}'
|
||||
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'branch_head', return_value='a' * 40), \
|
||||
patch.object(deploy, 'validate_images'), patch.object(deploy, 'inspect_services', return_value=running), \
|
||||
@@ -249,5 +302,42 @@ class MirrorTests(unittest.TestCase):
|
||||
self.assertEqual(before, mirror.refs(self.target))
|
||||
|
||||
|
||||
class TagRecoveryTests(unittest.TestCase):
|
||||
def test_recovery_converts_known_legacy_search_reference(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
ci.save(directory / 'deployment.pending.json', {})
|
||||
previous = {'version': '20261101-aaaaaaaa', 'images': {part: {'digest': 'synthetic'} for part in ci.IMAGES}}
|
||||
content = 'services:\n searxng:\n image: docker.io/searxng/searxng@' + deploy.SEARXNG_DIGEST + '\n'
|
||||
with patch.object(deploy, 'validate_images'), patch.object(deploy, 'validate_search_image'), \
|
||||
patch.object(deploy, 'health'), patch.object(deploy, 'run', return_value=''):
|
||||
deploy.restore(directory, {'previous': previous, 'previous_compose': content, 'previous_running': ['searxng']})
|
||||
restored = (directory / 'compose.yml').read_text()
|
||||
self.assertIn(deploy.SEARXNG_IMAGE, restored)
|
||||
self.assertNotIn('@sha256:', restored)
|
||||
self.assertNotIn('@sha256:', (directory / 'images.env').read_text())
|
||||
|
||||
def test_unknown_legacy_reference_cannot_restart_containers(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
ci.save(directory / 'deployment.pending.json', {})
|
||||
content = 'services:\n searxng:\n image: docker.io/searxng/searxng@sha256:' + '0' * 64 + '\n'
|
||||
with patch.object(deploy, 'validate_images'), patch.object(deploy, 'run', return_value='') as run:
|
||||
with self.assertRaisesRegex(RuntimeError, 'unknown digest'):
|
||||
deploy.restore(directory, {'previous': {}, 'previous_compose': content, 'previous_running': ['searxng']})
|
||||
self.assertFalse(any('up' in c.args for c in run.call_args_list))
|
||||
self.assertTrue((directory / 'deployment.pending.json').exists())
|
||||
|
||||
def test_search_tag_replacement_is_rejected(self):
|
||||
for digest in (deploy.SEARXNG_DIGEST, 'sha256:' + '0' * 64):
|
||||
with self.subTest(digest=digest), patch.object(deploy, 'run', return_value=json.dumps([{'RepoDigests': ['searxng/searxng@' + digest]}])) as run:
|
||||
if digest == deploy.SEARXNG_DIGEST:
|
||||
deploy.validate_search_image()
|
||||
else:
|
||||
with self.assertRaisesRegex(RuntimeError, 'Search version tag'):
|
||||
deploy.validate_search_image()
|
||||
self.assertNotIn('@sha256:', run.call_args.args[-1])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -58,9 +58,8 @@ class EntrypointTests(unittest.TestCase):
|
||||
self.write_receipt()
|
||||
(self.directory / 'compose.yml').write_text('name: alphapulse-dev\nservices: {}\n')
|
||||
(self.directory / 'images.env').write_text('SYNTHETIC=only\n')
|
||||
self.expected = {name: instance.SEARCH_IMAGE if name == 'searxng' else self.receipt['images'][
|
||||
'web' if name == 'web' else 'backend']['digest'] for name in instance.SERVICES}
|
||||
images = {image: {'Id': 'local-' + str(i), 'Architecture': 'amd64', 'Os': 'linux', 'Config': {'Labels': {
|
||||
self.expected = {name: instance.SEARCH_IMAGE if name == 'searxng' else deploy.IMAGES['web' if name == 'web' else 'backend'] + ':' + self.receipt['version'] for name in instance.SERVICES}
|
||||
images = {image: {'Id': 'local-' + str(i), 'Architecture': 'amd64', 'Os': 'linux', 'RepoDigests': [self.receipt['images']['web' if image.startswith(deploy.IMAGES['web'] + ':') else 'backend']['digest']] if image != instance.SEARCH_IMAGE else ['searxng/searxng@' + instance.SEARCH_DIGEST], 'Config': {'Labels': {
|
||||
'org.opencontainers.image.revision': self.receipt['commit'],
|
||||
'org.opencontainers.image.version': self.receipt['version']}}} for i, image in enumerate(set(self.expected.values()))}
|
||||
self.fixture = {'config': {'name': 'alphapulse-dev', 'services': {s: {'image': image} for s, image in self.expected.items()}},
|
||||
@@ -100,16 +99,24 @@ class EntrypointTests(unittest.TestCase):
|
||||
self.assertEqual(up[up.index('--env-file') + 1], str(self.directory / 'images.env'))
|
||||
self.assertEqual(calls[-1]['args'][0], 'inspect')
|
||||
|
||||
def test_replaced_search_tag_never_starts_any_service(self):
|
||||
self.fixture['images'][instance.SEARCH_IMAGE]['RepoDigests'] = []
|
||||
calls = self.invoke(code=1)
|
||||
self.assertFalse(any('up' in c['args'] for c in calls))
|
||||
|
||||
def test_missing_or_mismatched_images_never_start_any_service(self):
|
||||
backend = self.receipt['images']['backend']['digest']
|
||||
for case in ('missing', 'labels', 'selection'):
|
||||
backend = deploy.IMAGES['backend'] + ':' + self.receipt['version']
|
||||
for case in ('missing', 'labels', 'selection', 'digest'):
|
||||
with self.subTest(case=case):
|
||||
if case == 'missing': self.fixture['missing_image'] = backend
|
||||
if case == 'labels': self.fixture['images'][backend]['Config']['Labels']['org.opencontainers.image.revision'] = 'b' * 40
|
||||
if case == 'selection': self.fixture['config']['services']['api']['image'] = 'unaccepted'
|
||||
if case == 'digest': self.fixture['images'][backend]['RepoDigests'] = []
|
||||
calls = self.invoke(code=1)
|
||||
self.assertFalse(any('up' in c['args'] for c in calls))
|
||||
self.fixture['config']['services']['api']['image'] = backend
|
||||
self.fixture.pop('missing_image', None)
|
||||
self.fixture['images'][backend]['RepoDigests'] = [self.receipt['images']['backend']['digest']]
|
||||
self.fixture['images'][backend]['Config']['Labels']['org.opencontainers.image.revision'] = 'a' * 40
|
||||
|
||||
def test_receipt_must_be_verified_before_docker(self):
|
||||
@@ -197,6 +204,7 @@ class InstallationTests(unittest.TestCase):
|
||||
def test_installed_helper_is_self_contained_and_service_set_matches_cd(self):
|
||||
self.assertEqual(set(instance.SERVICES), set(deploy.SERVICES))
|
||||
self.assertEqual(instance.SEARCH_IMAGE, deploy.SEARXNG_IMAGE)
|
||||
self.assertEqual(instance.SEARCH_DIGEST, deploy.SEARXNG_DIGEST)
|
||||
subprocess.run(['bash', '-n', str(deploy.ROOT / 'deploy/start.sh'), str(deploy.ROOT / 'deploy/stop.sh')], check=True)
|
||||
|
||||
def test_install_only_does_not_use_docker_or_change_deployment_receipt(self):
|
||||
@@ -213,9 +221,9 @@ class InstallationTests(unittest.TestCase):
|
||||
def test_first_deployment_installs_versioned_entries(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
build = {'commit': 'a' * 40, 'images': {part: {'digest': 'synthetic'} for part in deploy.IMAGES}}
|
||||
build = {'version':'20261101-aaaaaaaa', 'commit': 'a' * 40, 'images': {part: {'digest': 'synthetic'} for part in deploy.IMAGES}}
|
||||
with patch.object(deploy, 'INSTANCE', directory), patch.object(deploy, 'branch_head', return_value=build['commit']), \
|
||||
patch.object(deploy, 'validate_images'), patch.object(deploy, 'run', return_value=''), \
|
||||
patch.object(deploy, 'validate_images'), patch.object(deploy, 'run', return_value=json.dumps([{'RepoDigests': ['searxng/searxng@' + deploy.SEARXNG_DIGEST]}])), \
|
||||
patch.object(deploy, 'health'), patch.object(deploy.time, 'sleep'):
|
||||
deploy.deploy(build, directory)
|
||||
for name in ('start.sh', 'stop.sh', 'instance.py'):
|
||||
|
||||
Reference in new issue
Block a user