feat: replace profiles with kubernetes-style secrets
Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).
- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,9 +86,6 @@ servers:
|
||||
servers:
|
||||
- name: test
|
||||
transport: STDIO
|
||||
profiles:
|
||||
- name: default
|
||||
server: test
|
||||
`);
|
||||
|
||||
const cmd = createApplyCommand({ client, log });
|
||||
@@ -97,52 +94,51 @@ profiles:
|
||||
expect(client.post).not.toHaveBeenCalled();
|
||||
expect(output.join('\n')).toContain('Dry run');
|
||||
expect(output.join('\n')).toContain('1 server(s)');
|
||||
expect(output.join('\n')).toContain('1 profile(s)');
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('applies profiles with server lookup', async () => {
|
||||
it('applies secrets', async () => {
|
||||
const configPath = join(tmpDir, 'config.yaml');
|
||||
writeFileSync(configPath, `
|
||||
secrets:
|
||||
- name: ha-creds
|
||||
data:
|
||||
TOKEN: abc123
|
||||
URL: https://ha.local
|
||||
`);
|
||||
|
||||
const cmd = createApplyCommand({ client, log });
|
||||
await cmd.parseAsync([configPath], { from: 'user' });
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/secrets', expect.objectContaining({
|
||||
name: 'ha-creds',
|
||||
data: { TOKEN: 'abc123', URL: 'https://ha.local' },
|
||||
}));
|
||||
expect(output.join('\n')).toContain('Created secret: ha-creds');
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('updates existing secrets', async () => {
|
||||
vi.mocked(client.get).mockImplementation(async (url: string) => {
|
||||
if (url === '/api/v1/servers') return [{ id: 'srv-1', name: 'slack' }];
|
||||
if (url === '/api/v1/secrets') return [{ id: 'sec-1', name: 'ha-creds' }];
|
||||
return [];
|
||||
});
|
||||
|
||||
const configPath = join(tmpDir, 'config.yaml');
|
||||
writeFileSync(configPath, `
|
||||
profiles:
|
||||
- name: default
|
||||
server: slack
|
||||
envOverrides:
|
||||
SLACK_TOKEN: "xoxb-test"
|
||||
secrets:
|
||||
- name: ha-creds
|
||||
data:
|
||||
TOKEN: new-token
|
||||
`);
|
||||
|
||||
const cmd = createApplyCommand({ client, log });
|
||||
await cmd.parseAsync([configPath], { from: 'user' });
|
||||
|
||||
expect(client.post).toHaveBeenCalledWith('/api/v1/profiles', expect.objectContaining({
|
||||
name: 'default',
|
||||
serverId: 'srv-1',
|
||||
envOverrides: { SLACK_TOKEN: 'xoxb-test' },
|
||||
}));
|
||||
expect(output.join('\n')).toContain('Created profile: default');
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('skips profiles when server not found', async () => {
|
||||
const configPath = join(tmpDir, 'config.yaml');
|
||||
writeFileSync(configPath, `
|
||||
profiles:
|
||||
- name: default
|
||||
server: nonexistent
|
||||
`);
|
||||
|
||||
const cmd = createApplyCommand({ client, log });
|
||||
await cmd.parseAsync([configPath], { from: 'user' });
|
||||
|
||||
expect(client.post).not.toHaveBeenCalled();
|
||||
expect(output.join('\n')).toContain("Skipping profile 'default'");
|
||||
expect(client.put).toHaveBeenCalledWith('/api/v1/secrets/sec-1', { data: { TOKEN: 'new-token' } });
|
||||
expect(output.join('\n')).toContain('Updated secret: ha-creds');
|
||||
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user