feat: store backup config in DB secret instead of env var
Move backup SSH keys and repo URL from MCPD_BACKUP_REPO env var to a "backup-ssh" secret in the database. Keys are auto-generated on first init and stored back into the secret. Also fix ERR_HTTP_HEADERS_SENT crash caused by reply.send() without return in routes when onSend hook is registered. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ export interface BackupDeps {
|
||||
interface BackupStatus {
|
||||
enabled: boolean;
|
||||
repoUrl: string | null;
|
||||
publicKey: string | null;
|
||||
gitReachable: boolean;
|
||||
lastSyncAt: string | null;
|
||||
lastPushAt: string | null;
|
||||
@@ -33,12 +34,12 @@ export function createBackupCommand(deps: BackupDeps): Command {
|
||||
if (!status.enabled) {
|
||||
deps.log('Backup: disabled');
|
||||
deps.log('');
|
||||
deps.log('To enable, set MCPD_BACKUP_REPO in your mcpd environment:');
|
||||
deps.log(' 1. Create a bare git repo (e.g. on Gitea, GitHub, or local)');
|
||||
deps.log(' 2. Add MCPD_BACKUP_REPO=<repo-url> to stack/.env');
|
||||
deps.log(' 3. Redeploy mcpd (bash deploy.sh)');
|
||||
deps.log(' 4. Run: mcpctl backup key');
|
||||
deps.log(' Add the SSH key as a deploy key (with write access) in your git host');
|
||||
deps.log('To enable, create a backup-ssh secret:');
|
||||
deps.log(' mcpctl create secret backup-ssh --data repoUrl=ssh://git@host/repo.git');
|
||||
deps.log('');
|
||||
deps.log('After creating the secret, restart mcpd. An SSH keypair will be');
|
||||
deps.log('auto-generated and stored in the secret. Run mcpctl backup to see');
|
||||
deps.log('the public key, then add it as a deploy key in your git host.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,6 +66,10 @@ export function createBackupCommand(deps: BackupDeps): Command {
|
||||
if (status.lastError) {
|
||||
deps.log(`Error: ${status.lastError}`);
|
||||
}
|
||||
if (status.publicKey) {
|
||||
deps.log('');
|
||||
deps.log(`SSH key: ${status.publicKey}`);
|
||||
}
|
||||
});
|
||||
|
||||
cmd
|
||||
@@ -109,16 +114,6 @@ export function createBackupCommand(deps: BackupDeps): Command {
|
||||
}
|
||||
});
|
||||
|
||||
cmd
|
||||
.command('key')
|
||||
.description('Show SSH public key for deploy key setup')
|
||||
.action(async () => {
|
||||
const { publicKey } = await deps.client.get<{ publicKey: string }>('/api/v1/backup/key');
|
||||
deps.log(publicKey);
|
||||
deps.log('');
|
||||
deps.log('Add this key as a deploy key (with write access) in your Git hosting provider.');
|
||||
});
|
||||
|
||||
// ── Restore subcommand group ──
|
||||
const restore = new Command('restore')
|
||||
.description('Restore mcpctl state from backup history');
|
||||
|
||||
@@ -73,13 +73,38 @@ describe('backup command', () => {
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('5 changes pending'));
|
||||
});
|
||||
|
||||
it('shows SSH public key', async () => {
|
||||
mockClient.get.mockResolvedValue({ publicKey: 'ssh-ed25519 AAAA... mcpd@mcpctl.local' });
|
||||
it('shows SSH public key in status when enabled', async () => {
|
||||
mockClient.get.mockResolvedValue({
|
||||
enabled: true,
|
||||
repoUrl: 'ssh://git@host/repo.git',
|
||||
publicKey: 'ssh-ed25519 AAAA... mcpd@mcpctl.local',
|
||||
gitReachable: true,
|
||||
lastSyncAt: null,
|
||||
lastPushAt: null,
|
||||
lastError: null,
|
||||
pendingCount: 0,
|
||||
});
|
||||
|
||||
await makeCmd().parseAsync(['key'], { from: 'user' });
|
||||
await makeCmd().parseAsync([], { from: 'user' });
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/key');
|
||||
expect(log).toHaveBeenCalledWith('ssh-ed25519 AAAA... mcpd@mcpctl.local');
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('ssh-ed25519 AAAA... mcpd@mcpctl.local'));
|
||||
});
|
||||
|
||||
it('shows setup instructions when disabled', async () => {
|
||||
mockClient.get.mockResolvedValue({
|
||||
enabled: false,
|
||||
repoUrl: null,
|
||||
publicKey: null,
|
||||
gitReachable: false,
|
||||
lastSyncAt: null,
|
||||
lastPushAt: null,
|
||||
lastError: null,
|
||||
pendingCount: 0,
|
||||
});
|
||||
|
||||
await makeCmd().parseAsync([], { from: 'user' });
|
||||
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining('mcpctl create secret backup-ssh'));
|
||||
});
|
||||
|
||||
it('shows commit log', async () => {
|
||||
|
||||
Reference in New Issue
Block a user