Support managed Python and remote Docker in CI; stop collector promptly
This commit is contained in:
@@ -13,7 +13,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: |
|
||||
python3 -m pip install --user 'PyYAML==6.0.3'
|
||||
python3 -m pip install --target "${RUNNER_TEMP}/python" 'PyYAML==6.0.3'
|
||||
echo "PYTHONPATH=${RUNNER_TEMP}/python" >> "$GITHUB_ENV"
|
||||
curl -fsSL https://dl.k8s.io/release/v1.35.3/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl
|
||||
chmod 0755 /usr/local/bin/kubectl
|
||||
- name: Test policy and boot the actual game configuration
|
||||
@@ -36,7 +37,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: |
|
||||
python3 -m pip install --user 'PyYAML==6.0.3'
|
||||
python3 -m pip install --target "${RUNNER_TEMP}/python" 'PyYAML==6.0.3'
|
||||
echo "PYTHONPATH=${RUNNER_TEMP}/python" >> "$GITHUB_ENV"
|
||||
curl -fsSL https://dl.k8s.io/release/v1.35.3/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl
|
||||
chmod 0755 /usr/local/bin/kubectl
|
||||
- name: Deploy using the dedicated Factorio service account
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Only ordinary console commands: no /c, /sc, Lua, mods, or achievement changes.
|
||||
set -eu
|
||||
trap 'exit 0' TERM INT
|
||||
metrics_dir=${METRICS_DIR:-/metrics}
|
||||
mkdir -p "$metrics_dir"
|
||||
while :; do
|
||||
|
||||
+59
-37
@@ -1,43 +1,65 @@
|
||||
"""Boot the actual server image with the actual rendered configuration, then probe/save/restart it."""
|
||||
import json,os,secrets,subprocess,sys,tempfile,time
|
||||
"""Boot the rendered image/configuration via Docker, including remote Docker sockets."""
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
docs=list(yaml.safe_load_all(Path(sys.argv[1]).read_text()))
|
||||
d=next(d for d in docs if d['kind']=='Deployment')
|
||||
c=next(c for c in d['spec']['template']['spec']['containers'] if c['name']=='factorio')
|
||||
cms={d['metadata']['name']:d['data'] for d in docs if d['kind']=='ConfigMap'}
|
||||
run=lambda *a,**kw: subprocess.run(a,check=True,**kw)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory)
|
||||
for name in ['scripts','settings','credentials','data']:(root/name).mkdir()
|
||||
for folder,name in [('scripts','factorio-scripts'),('settings','factorio-settings')]:
|
||||
for key,value in cms[name].items():
|
||||
file=root/folder/key; file.write_text(value); file.chmod(0o755 if key.endswith('.sh') else 0o644)
|
||||
(root/'credentials/rcon-password').write_text(secrets.token_urlsafe(32))
|
||||
args=['docker','run','--detach','--name','factorio-smoke-'+secrets.token_hex(5)]
|
||||
for env in c['env']:args+=['--env',env['name']+'='+env['value']]
|
||||
for mount in c['volumeMounts']:
|
||||
folder={'data':'data','scripts':'scripts','settings':'settings','credentials':'credentials'}[mount['name']]
|
||||
args+=['--volume',f'{root/folder}:{mount["mountPath"]}'+(':ro' if mount.get('readOnly') else '')]
|
||||
args+=['--entrypoint',c['command'][0],c['image'],*c['command'][1:]]
|
||||
container=subprocess.check_output(args,text=True).strip()
|
||||
def ready():
|
||||
command=c['readinessProbe']['exec']['command']
|
||||
for _ in range(90):
|
||||
if subprocess.run(['docker','exec',container,*command],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15).returncode==0:return
|
||||
time.sleep(2)
|
||||
raise RuntimeError('Server did not pass the production readiness probe')
|
||||
try:
|
||||
docs = list(yaml.safe_load_all(Path(sys.argv[1]).read_text()))
|
||||
d = next(d for d in docs if d['kind'] == 'Deployment')
|
||||
c = next(c for c in d['spec']['template']['spec']['containers'] if c['name'] == 'factorio')
|
||||
cms = {d['metadata']['name']: d['data'] for d in docs if d['kind'] == 'ConfigMap'}
|
||||
run = lambda *a, **kw: subprocess.run(a, check=True, **kw)
|
||||
volume = 'factorio-smoke-' + secrets.token_hex(6)
|
||||
run('docker', 'volume', 'create', volume, stdout=subprocess.DEVNULL)
|
||||
container = None
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in ['scripts', 'settings', 'credentials']:
|
||||
(root / name).mkdir()
|
||||
for folder, name in [('scripts', 'factorio-scripts'), ('settings', 'factorio-settings')]:
|
||||
for key, value in cms[name].items():
|
||||
file = root / folder / key
|
||||
file.write_text(value)
|
||||
file.chmod(0o755 if key.endswith('.sh') else 0o644)
|
||||
(root / 'credentials/rcon-password').write_text(secrets.token_urlsafe(32))
|
||||
args = ['docker', 'create']
|
||||
for env in c['env']:
|
||||
args += ['--env', env['name'] + '=' + env['value']]
|
||||
data_mount = next(m['mountPath'] for m in c['volumeMounts'] if m['name'] == 'data')
|
||||
args += ['--volume', f'{volume}:{data_mount}']
|
||||
args += ['--entrypoint', c['command'][0], c['image'], *c['command'][1:]]
|
||||
container = subprocess.check_output(args, text=True).strip()
|
||||
# docker cp works when the daemon is outside the CI job container;
|
||||
# bind-mounting the job container's /tmp would silently mount a different directory.
|
||||
for mount in c['volumeMounts']:
|
||||
if mount['name'] != 'data':
|
||||
run('docker', 'cp', str(root / mount['name']), container + ':' + mount['mountPath'])
|
||||
run('docker', 'start', container, stdout=subprocess.DEVNULL)
|
||||
|
||||
def ready():
|
||||
command = c['readinessProbe']['exec']['command']
|
||||
for _ in range(90):
|
||||
if subprocess.run(['docker', 'exec', container, *command], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=15).returncode == 0:
|
||||
return
|
||||
time.sleep(2)
|
||||
raise RuntimeError('Server did not pass the production readiness probe')
|
||||
|
||||
ready()
|
||||
run('docker','exec',container,'rcon','/server-save')
|
||||
run('docker','stop','--time','180',container,stdout=subprocess.DEVNULL)
|
||||
assert list((root/'data/saves').glob('*.zip')), 'No persistent save'
|
||||
run('docker','start',container,stdout=subprocess.DEVNULL)
|
||||
run('docker', 'exec', container, 'rcon', '/server-save')
|
||||
run('docker', 'stop', '--time', str(d['spec']['template']['spec']['terminationGracePeriodSeconds']),
|
||||
container, stdout=subprocess.DEVNULL)
|
||||
run('docker', 'start', container, stdout=subprocess.DEVNULL)
|
||||
ready()
|
||||
assert list((root/'data/upgrade-backups').glob('*/*.zip')), 'No pre-upgrade backup'
|
||||
run('docker', 'exec', container, 'sh', '-c',
|
||||
'test -n "$(find "$1/saves" -name "*.zip")" && test -n "$(find "$1/upgrade-backups" -name "*.zip")"',
|
||||
'sh', data_mount)
|
||||
print('Actual image passed RCON readiness, persistent save, restart, and upgrade-backup checks')
|
||||
finally:
|
||||
run('docker','rm','--force',container,stdout=subprocess.DEVNULL)
|
||||
# The upstream image owns its world files; cleanup through the same image.
|
||||
run('docker','run','--rm','--volume',f'{root}/data:/cleanup','--entrypoint','sh',c['image'],'-c','rm -rf /cleanup/*',stdout=subprocess.DEVNULL)
|
||||
finally:
|
||||
if container:
|
||||
run('docker', 'rm', '--force', container, stdout=subprocess.DEVNULL)
|
||||
run('docker', 'volume', 'rm', volume, stdout=subprocess.DEVNULL)
|
||||
|
||||
@@ -14,7 +14,7 @@ class CollectorTests(unittest.TestCase):
|
||||
fake=root/'rcon'
|
||||
fake.write_text('#!/bin/sh\n[ "$*" = "/players online" ] || exit 88\nprintf "%s\\n" "$RESPONSE"\nexit "$CODE"\n')
|
||||
fake.chmod(0o755)
|
||||
proc=subprocess.Popen(['sh',str(SCRIPT)],env={**os.environ,'PATH':tmp+':'+os.environ['PATH'],'METRICS_DIR':tmp,'RESPONSE':response,'CODE':str(code)},start_new_session=True)
|
||||
proc=subprocess.Popen(['sh',str(SCRIPT)],env={**os.environ,'PATH':tmp+':'+os.environ['PATH'],'METRICS_DIR':tmp,'RESPONSE':response,'CODE':str(code)},start_new_session=True,stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
output=root/'factorio.prom'
|
||||
for _ in range(100):
|
||||
|
||||
Reference in New Issue
Block a user