fix: 完善低权限备份恢复与只读部署密钥支持

这个提交包含在:
wikiboss
2026-10-28 18:02:03 +08:00
父节点 d5f108b0c5
当前提交 d339926490
共有 3 个文件被更改,包括 20 次插入 和 17 次删除

查看文件

@@ -14,7 +14,7 @@
- 程序:`/opt/grain-auto-deploy/auto_deploy.py` - 程序:`/opt/grain-auto-deploy/auto_deploy.py`
- 配置:`/etc/grain-auto-deploy.json` - 配置:`/etc/grain-auto-deploy.json`
- 版本及文件清单:`/var/lib/grain-auto-deploy/backend.json`、`frontend.json` - 版本及文件清单:`/var/lib/grain-auto-deploy/backend.json`、`frontend.json`
- 备份:`/www/backup/grain-auto-deploy/` - 备份:`/var/lib/grain-auto-deploy/backups/`
- 日志:`/var/log/grain-auto-deploy.log`,以及 `journalctl -u grain-auto-deploy.service` - 日志:`/var/log/grain-auto-deploy.log`,以及 `journalctl -u grain-auto-deploy.service`
运维命令: 运维命令:

查看文件

@@ -9,6 +9,7 @@ import logging
import os import os
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
import shutil import shutil
import shlex
import subprocess import subprocess
import tarfile import tarfile
import tempfile import tempfile
@@ -64,6 +65,17 @@ def healthcheck(url, host):
raise RuntimeError('Granary API health check failed') raise RuntimeError('Granary API health check failed')
def replace_file(source, live):
live.parent.mkdir(parents=True, exist_ok=True)
temp = live.with_name(live.name + '.deploy-tmp')
shutil.copyfile(str(source), str(temp))
mode = live.stat().st_mode & 0o777 if live.exists() else 0o644
os.chmod(str(temp), mode)
# The service runs as www; replacing an older root-uploaded file must not
# attempt to chown it back to root or require elevated permissions.
os.replace(str(temp), str(live))
def publish(config, repo): def publish(config, repo):
state_dir = Path(config['state']) state_dir = Path(config['state'])
cache = state_dir / (repo['name'] + '.git') cache = state_dir / (repo['name'] + '.git')
@@ -71,6 +83,10 @@ def publish(config, repo):
if not cache.exists(): if not cache.exists():
run(['git', 'init', '--bare', str(cache)]) run(['git', 'init', '--bare', str(cache)])
git = ['git', '--git-dir=' + str(cache)] git = ['git', '--git-dir=' + str(cache)]
if repo.get('key'):
ssh = 'ssh -i {} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile={}'.format(
shlex.quote(repo['key']), shlex.quote(repo['known_hosts']))
git += ['-c', 'core.sshCommand=' + ssh]
run(git + ['fetch', '--depth=1', repo['url'], '+refs/heads/master:refs/heads/deploy']) run(git + ['fetch', '--depth=1', repo['url'], '+refs/heads/master:refs/heads/deploy'])
revision = run(git + ['rev-parse', 'refs/heads/deploy']).decode().strip() revision = run(git + ['rev-parse', 'refs/heads/deploy']).decode().strip()
state_path = state_dir / (repo['name'] + '.json') state_path = state_dir / (repo['name'] + '.json')
@@ -136,20 +152,7 @@ def publish(config, repo):
try: try:
for name in changes: for name in changes:
live = destination(root, name) live = destination(root, name)
live.parent.mkdir(parents=True, exist_ok=True) replace_file(stage / name, live)
temp = live.with_name(live.name + '.deploy-tmp')
shutil.copyfile(str(stage / name), str(temp))
if live.exists():
stat = live.stat()
os.chmod(str(temp), stat.st_mode & 0o777)
if hasattr(os, 'chown'):
os.chown(str(temp), stat.st_uid, stat.st_gid)
else:
os.chmod(str(temp), 0o644)
if hasattr(os, 'chown'):
parent = live.parent.stat()
os.chown(str(temp), parent.st_uid, parent.st_gid)
os.replace(str(temp), str(live))
for name in deleted: for name in deleted:
live = destination(root, name) live = destination(root, name)
if live.is_file(): if live.is_file():
@@ -160,7 +163,7 @@ def publish(config, repo):
for record in reversed(records): for record in reversed(records):
live = destination(root, record['path']) live = destination(root, record['path'])
if record['existed']: if record['existed']:
shutil.copy2(str(backup / 'files' / record['path']), str(live)) replace_file(backup / 'files' / record['path'], live)
elif live.is_file(): elif live.is_file():
live.unlink() live.unlink()
LOG.exception('%s rolled back; backup=%s', repo['name'], backup) LOG.exception('%s rolled back; backup=%s', repo['name'], backup)

查看文件

@@ -14,6 +14,6 @@ NoNewPrivileges=true
PrivateTmp=true PrivateTmp=true
ProtectHome=true ProtectHome=true
ProtectSystem=strict ProtectSystem=strict
ReadWritePaths=/var/lib/grain-auto-deploy /www/backup/grain-auto-deploy /www/wwwroot/grain.gynhc.com /var/log/grain-auto-deploy.log ReadWritePaths=/var/lib/grain-auto-deploy /www/wwwroot/grain.gynhc.com /var/log/grain-auto-deploy.log
ReadOnlyPaths=/www/wwwroot/grain.gynhc.com/.git /www/wwwroot/grain.gynhc.com/application/database.php /www/wwwroot/grain.gynhc.com/application/config.php /www/wwwroot/grain.gynhc.com/application/extra/site.php ReadOnlyPaths=/www/wwwroot/grain.gynhc.com/.git /www/wwwroot/grain.gynhc.com/application/database.php /www/wwwroot/grain.gynhc.com/application/config.php /www/wwwroot/grain.gynhc.com/application/extra/site.php
RestrictSUIDSGID=true RestrictSUIDSGID=true