@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/drizzle/0009_dns_registrations.sql +13 -0
- package/drizzle/0010_dns_internal_records.sql +12 -0
- package/drizzle/0011_backups_name.sql +1 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +3 -3
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +211 -1
- package/src/api-clients/proxmox.ts +399 -8
- package/src/cli/command-registry.ts +83 -6
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +89 -24
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-publish.ts +24 -0
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +26 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
- package/src/cli/commands/publish/alpha.test.ts +26 -0
- package/src/cli/commands/publish/alpha.ts +23 -0
- package/src/cli/commands/publish/types.ts +7 -2
- package/src/cli/commands/publish/workspace.ts +11 -1
- package/src/cli/commands/restore.ts +29 -0
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +283 -209
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +252 -129
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-doctor.ts +135 -40
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/commands/system-migrate.test.ts +40 -0
- package/src/cli/commands/system-migrate.ts +65 -0
- package/src/cli/completion.ts +23 -0
- package/src/cli/index.ts +91 -7
- package/src/cli/service-credential.ts +54 -0
- package/src/config/paths.test.ts +61 -48
- package/src/db/client.ts +15 -146
- package/src/db/migrate.ts +14 -6
- package/src/db/schema-introspection.ts +88 -0
- package/src/db/schema.ts +74 -0
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/hooks/capability-loader.ts +43 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +17 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-interview.ts +232 -0
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-preflight.ts +25 -0
- package/src/services/deploy-validation.test.ts +54 -4
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/dns-internal-records.test.ts +126 -0
- package/src/services/dns-internal-records.ts +119 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-provider-backfill.ts +14 -2
- package/src/services/dns-registrations.test.ts +120 -0
- package/src/services/dns-registrations.ts +108 -0
- package/src/services/events-daemon.test.ts +59 -0
- package/src/services/events-daemon.ts +191 -57
- package/src/services/fleet-checks.test.ts +508 -0
- package/src/services/fleet-checks.ts +678 -0
- package/src/services/module-build.test.ts +43 -38
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/module-subscriptions.test.ts +88 -0
- package/src/services/module-subscriptions.ts +50 -1
- package/src/services/module-validator/bundled-deps.test.ts +55 -0
- package/src/services/module-validator/bundled-deps.ts +115 -0
- package/src/services/module-validator/capability-versions.test.ts +1 -1
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +92 -12
- package/src/templates/generator.ts +165 -80
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
|
@@ -3,31 +3,104 @@
|
|
|
3
3
|
* Re-run the interactive interview for an existing service's provider configuration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import {
|
|
8
7
|
type ProxmoxCredentials,
|
|
8
|
+
type ProxmoxVmTemplate,
|
|
9
9
|
buildTemplatePath,
|
|
10
10
|
extractTemplateFilename,
|
|
11
11
|
listAvailableTemplates,
|
|
12
12
|
listNodeStorage,
|
|
13
13
|
} from '../../api-clients/proxmox';
|
|
14
|
+
import { askConfirm, askSelect, askText, withInterviewSession } from '../../services/bus-interview';
|
|
14
15
|
import {
|
|
15
16
|
getContainerServiceByServiceId,
|
|
16
17
|
getServiceCredentials,
|
|
17
18
|
updateServiceProviderConfig,
|
|
18
19
|
} from '../../services/container-service';
|
|
19
|
-
import { celiloIntro, celiloOutro
|
|
20
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
20
21
|
import type { CommandResult } from '../types';
|
|
21
|
-
import { validateRequired } from '../validators';
|
|
22
22
|
import {
|
|
23
23
|
runApplianceDownload,
|
|
24
24
|
selectUbuntuApplianceFromCatalog,
|
|
25
25
|
} from './proxmox-template-selection';
|
|
26
|
+
import { buildCloudInitTemplate, detectExistingVmTemplates } from './proxmox-vm-template-build';
|
|
26
27
|
|
|
27
|
-
interface
|
|
28
|
+
// A `type` (not `interface`) so it carries an implicit index signature and is
|
|
29
|
+
// assignable to the loose `Record<string, unknown>` providerConfig column.
|
|
30
|
+
export type ProxmoxProviderConfig = {
|
|
28
31
|
default_target_node: string;
|
|
29
32
|
lxc_template: string;
|
|
30
33
|
storage: string;
|
|
34
|
+
// VM template to clone for `requires.system.type: vm` modules. Optional —
|
|
35
|
+
// a service may only ever host LXC modules. MUST be carried through a
|
|
36
|
+
// reconfigure (ISS-0128): updateServiceProviderConfig replaces the whole
|
|
37
|
+
// column, so dropping it here silently deletes the operator's template.
|
|
38
|
+
vm_template?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Sentinel choices for the VM-template reconfigure step (non-template values
|
|
42
|
+
// returned by the select). Any other value is an existing template name.
|
|
43
|
+
const VM_KEEP = '__keep__';
|
|
44
|
+
const VM_BUILD = '__build__';
|
|
45
|
+
const VM_CLEAR = '__clear__';
|
|
46
|
+
const VM_SKIP = '__skip__';
|
|
47
|
+
|
|
48
|
+
export interface VmTemplateChoice {
|
|
49
|
+
value: string;
|
|
50
|
+
label: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build the select options for the VM-template reconfigure step, given the
|
|
55
|
+
* current value and the templates detected on the node. Pure — no I/O — so the
|
|
56
|
+
* menu is unit-testable and a headless caller (ISS-0127) can resolve a choice
|
|
57
|
+
* without a prompt.
|
|
58
|
+
*/
|
|
59
|
+
export function buildVmTemplateChoices(
|
|
60
|
+
current: string | undefined,
|
|
61
|
+
existing: ProxmoxVmTemplate[],
|
|
62
|
+
): VmTemplateChoice[] {
|
|
63
|
+
const choices: VmTemplateChoice[] = [];
|
|
64
|
+
if (current) {
|
|
65
|
+
choices.push({ value: VM_KEEP, label: `Keep current (${current})` });
|
|
66
|
+
}
|
|
67
|
+
for (const template of existing) {
|
|
68
|
+
if (template.name === current) continue;
|
|
69
|
+
choices.push({ value: template.name, label: `${template.name} (VMID ${template.vmid})` });
|
|
70
|
+
}
|
|
71
|
+
choices.push({ value: VM_BUILD, label: 'Build a new Ubuntu 24.04 cloud-init template now' });
|
|
72
|
+
choices.push(
|
|
73
|
+
current
|
|
74
|
+
? { value: VM_CLEAR, label: 'Clear — remove the VM template' }
|
|
75
|
+
: { value: VM_SKIP, label: 'Skip — no VM template' },
|
|
76
|
+
);
|
|
77
|
+
return choices;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the full providerConfig to persist from the reconfigure interview's
|
|
82
|
+
* resolved values. The caller seeds every field from the current config (via the
|
|
83
|
+
* prompt defaults and the VM-template choice), so the result is a function of
|
|
84
|
+
* `edits` alone. vm_template is included only when set — a falsy value clears it
|
|
85
|
+
* rather than persisting an undefined key. This is the guard for the
|
|
86
|
+
* wholesale-replace drop bug (ISS-0128): vm_template is a first-class edit, not
|
|
87
|
+
* an afterthought that gets omitted from the written object.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveReconfiguredProviderConfig(edits: {
|
|
90
|
+
default_target_node: string;
|
|
91
|
+
lxc_template: string;
|
|
92
|
+
storage: string;
|
|
93
|
+
vm_template: string | undefined;
|
|
94
|
+
}): ProxmoxProviderConfig {
|
|
95
|
+
const config: ProxmoxProviderConfig = {
|
|
96
|
+
default_target_node: edits.default_target_node,
|
|
97
|
+
lxc_template: edits.lxc_template,
|
|
98
|
+
storage: edits.storage,
|
|
99
|
+
};
|
|
100
|
+
if (edits.vm_template) {
|
|
101
|
+
config.vm_template = edits.vm_template;
|
|
102
|
+
}
|
|
103
|
+
return config;
|
|
31
104
|
}
|
|
32
105
|
|
|
33
106
|
export async function handleServiceReconfigure(
|
|
@@ -42,146 +115,196 @@ export async function handleServiceReconfigure(
|
|
|
42
115
|
};
|
|
43
116
|
}
|
|
44
117
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
118
|
+
// Every prompt below is a bus interview (ISS-0127), so reconfigure is
|
|
119
|
+
// drivable headlessly via `celilo events respond --values` / `events reply`.
|
|
120
|
+
// `withInterviewSession` runs a terminal-responder when stdin is a TTY so
|
|
121
|
+
// those questions render locally; on a non-TTY run the headless responder
|
|
122
|
+
// answers instead.
|
|
123
|
+
const scope = `service:${serviceId}`;
|
|
50
124
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
125
|
+
return withInterviewSession(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const service = await getContainerServiceByServiceId(serviceId);
|
|
128
|
+
if (!service) {
|
|
129
|
+
return { success: false, error: `Service '${serviceId}' not found` };
|
|
130
|
+
}
|
|
57
131
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Show current configuration
|
|
64
|
-
console.log('Current configuration:');
|
|
65
|
-
console.log(` Target node: ${currentConfig.default_target_node}`);
|
|
66
|
-
console.log(` Storage: ${currentConfig.storage}`);
|
|
67
|
-
console.log(` Template: ${currentConfig.lxc_template}`);
|
|
68
|
-
console.log('');
|
|
69
|
-
|
|
70
|
-
// Re-prompt for configurable fields with current values as defaults
|
|
71
|
-
const targetNode = await promptText({
|
|
72
|
-
message: 'Default target node:',
|
|
73
|
-
defaultValue: currentConfig.default_target_node,
|
|
74
|
-
placeholder: currentConfig.default_target_node,
|
|
75
|
-
validate: validateRequired('Target node'),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const storage = await promptText({
|
|
79
|
-
message: 'Default storage:',
|
|
80
|
-
defaultValue: currentConfig.storage,
|
|
81
|
-
placeholder: currentConfig.storage,
|
|
82
|
-
validate: validateRequired('Storage'),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Find template storage. Done before catalog selection so the user only
|
|
86
|
-
// ever sees the chosen storage in subsequent messages.
|
|
87
|
-
console.log('\nFinding storage for templates...');
|
|
88
|
-
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
89
|
-
|
|
90
|
-
let templateStorage = 'local';
|
|
91
|
-
if (storageListResult.success) {
|
|
92
|
-
const vztmplStorage = storageListResult.data.find(
|
|
93
|
-
(s) => s.active && s.enabled && s.content.includes('vztmpl'),
|
|
94
|
-
);
|
|
95
|
-
if (vztmplStorage) {
|
|
96
|
-
templateStorage = vztmplStorage.storage;
|
|
97
|
-
console.log(`✓ Using storage '${templateStorage}' for templates`);
|
|
132
|
+
if (service.providerName !== 'proxmox') {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Service reconfigure is currently only supported for Proxmox services (got: ${service.providerName})`,
|
|
136
|
+
};
|
|
98
137
|
}
|
|
99
|
-
}
|
|
100
138
|
|
|
101
|
-
|
|
102
|
-
// matching the existing volid (e.g. user has 24.04 -1 cached, mirror has -2).
|
|
103
|
-
const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
|
|
104
|
-
currentTemplate: extractTemplateFilename(currentConfig.lxc_template),
|
|
105
|
-
});
|
|
106
|
-
if (selectionResult.kind === 'cancelled') {
|
|
107
|
-
p.cancel('Operation cancelled');
|
|
108
|
-
return { success: false, error: 'Cancelled by user' };
|
|
109
|
-
}
|
|
110
|
-
if (selectionResult.kind === 'error') {
|
|
111
|
-
return { success: false, error: selectionResult.message };
|
|
112
|
-
}
|
|
113
|
-
const templateFilename = selectionResult.choice.template;
|
|
114
|
-
const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
|
|
139
|
+
celiloIntro(`Reconfigure Service: ${service.name}`);
|
|
115
140
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
|
|
141
|
+
const credentials = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
|
|
142
|
+
const currentConfig = service.providerConfig as unknown as ProxmoxProviderConfig;
|
|
119
143
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
// Show current configuration
|
|
145
|
+
console.log('Current configuration:');
|
|
146
|
+
console.log(` Target node: ${currentConfig.default_target_node}`);
|
|
147
|
+
console.log(` Storage: ${currentConfig.storage}`);
|
|
148
|
+
console.log(` LXC template: ${currentConfig.lxc_template}`);
|
|
149
|
+
console.log(` VM template: ${currentConfig.vm_template ?? '(none)'}`);
|
|
150
|
+
console.log('');
|
|
124
151
|
|
|
125
|
-
|
|
126
|
-
|
|
152
|
+
// Re-prompt for configurable fields with current values as defaults
|
|
153
|
+
const targetNode = await askText({
|
|
154
|
+
scope,
|
|
155
|
+
key: 'default_target_node',
|
|
156
|
+
message: 'Default target node',
|
|
157
|
+
defaultValue: currentConfig.default_target_node,
|
|
158
|
+
placeholder: currentConfig.default_target_node,
|
|
159
|
+
required: true,
|
|
160
|
+
});
|
|
127
161
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
const storage = await askText({
|
|
163
|
+
scope,
|
|
164
|
+
key: 'storage',
|
|
165
|
+
message: 'Default storage',
|
|
166
|
+
defaultValue: currentConfig.storage,
|
|
167
|
+
placeholder: currentConfig.storage,
|
|
168
|
+
required: true,
|
|
131
169
|
});
|
|
132
170
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
// Find template storage. Done before catalog selection so the user only
|
|
172
|
+
// ever sees the chosen storage in subsequent messages.
|
|
173
|
+
console.log('\nFinding storage for templates...');
|
|
174
|
+
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
175
|
+
|
|
176
|
+
let templateStorage = 'local';
|
|
177
|
+
if (storageListResult.success) {
|
|
178
|
+
const vztmplStorage = storageListResult.data.find(
|
|
179
|
+
(s) => s.active && s.enabled && s.content.includes('vztmpl'),
|
|
136
180
|
);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
if (vztmplStorage) {
|
|
182
|
+
templateStorage = vztmplStorage.storage;
|
|
183
|
+
console.log(`✓ Using storage '${templateStorage}' for templates`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Pick a template from Proxmox's live catalog. Pre-selects the family
|
|
188
|
+
// matching the existing volid (e.g. user has 24.04 -1 cached, mirror has -2).
|
|
189
|
+
// The select + manual-entry fallback inside go through the bus interview
|
|
190
|
+
// too (ISS-0127), scoped to this service.
|
|
191
|
+
const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
|
|
192
|
+
scope,
|
|
193
|
+
currentTemplate: extractTemplateFilename(currentConfig.lxc_template),
|
|
194
|
+
});
|
|
195
|
+
if (selectionResult.kind === 'cancelled') {
|
|
196
|
+
return { success: false, error: 'Cancelled by user' };
|
|
197
|
+
}
|
|
198
|
+
if (selectionResult.kind === 'error') {
|
|
199
|
+
return { success: false, error: selectionResult.message };
|
|
200
|
+
}
|
|
201
|
+
const templateFilename = selectionResult.choice.template;
|
|
202
|
+
const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
|
|
203
|
+
|
|
204
|
+
// Check if new template exists in storage.
|
|
205
|
+
console.log(`\nChecking if template '${templateFilename}' exists...`);
|
|
206
|
+
const templatesResult = await listAvailableTemplates(
|
|
207
|
+
credentials,
|
|
208
|
+
targetNode,
|
|
209
|
+
templateStorage,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
let templateExists = false;
|
|
213
|
+
if (templatesResult.success) {
|
|
214
|
+
templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!templateExists) {
|
|
218
|
+
console.log(`✗ Template '${templateFilename}' not found`);
|
|
219
|
+
|
|
220
|
+
const shouldDownload = await askConfirm({
|
|
221
|
+
scope,
|
|
222
|
+
key: 'download_template',
|
|
223
|
+
message: 'Download template now?',
|
|
224
|
+
defaultValue: true,
|
|
143
225
|
});
|
|
144
226
|
|
|
145
|
-
if (!
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
227
|
+
if (!shouldDownload) {
|
|
228
|
+
console.log(
|
|
229
|
+
'\nTemplate not downloaded. Service will be updated but may fail verification.',
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
const outcome = await runApplianceDownload({
|
|
233
|
+
credentials,
|
|
234
|
+
targetNode,
|
|
235
|
+
templateStorage,
|
|
236
|
+
templateFilename,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!outcome.ready) {
|
|
240
|
+
const detail =
|
|
241
|
+
outcome.reason === 'task-failed'
|
|
242
|
+
? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}`
|
|
243
|
+
: outcome.reason === 'started-failed'
|
|
244
|
+
? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}`
|
|
245
|
+
: 'Template download did not complete in time';
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error: `${detail}\n\nTroubleshooting:\n 1. SSH into your Proxmox host and run: pveam update && pveam download ${templateStorage} ${templateFilename}\n 2. Re-try: celilo service reconfigure ${serviceId}\n 3. Check DNS and firewall settings on the Proxmox host`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
156
251
|
}
|
|
252
|
+
} else {
|
|
253
|
+
console.log(`✓ Template '${templateFilename}' found`);
|
|
157
254
|
}
|
|
158
|
-
} else {
|
|
159
|
-
console.log(`✓ Template '${templateFilename}' found`);
|
|
160
|
-
}
|
|
161
255
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
256
|
+
// VM template (for `requires.system.type: vm` modules). Keep the current
|
|
257
|
+
// value, swap to another detected template, build a new one, or clear it.
|
|
258
|
+
// Without this step a reconfigure would silently drop vm_template (ISS-0128).
|
|
259
|
+
let vmTemplate = currentConfig.vm_template;
|
|
260
|
+
const existingTemplates = await detectExistingVmTemplates(credentials, targetNode);
|
|
261
|
+
const vmChoice = await askSelect({
|
|
262
|
+
scope,
|
|
263
|
+
key: 'vm_template',
|
|
264
|
+
message: 'VM template for VM-type modules',
|
|
265
|
+
options: buildVmTemplateChoices(currentConfig.vm_template, existingTemplates),
|
|
266
|
+
});
|
|
267
|
+
if (vmChoice === VM_BUILD) {
|
|
268
|
+
vmTemplate = await buildCloudInitTemplate({
|
|
269
|
+
credentials,
|
|
270
|
+
nodeName: targetNode,
|
|
271
|
+
diskStorage: storage,
|
|
272
|
+
});
|
|
273
|
+
} else if (vmChoice === VM_CLEAR || vmChoice === VM_SKIP) {
|
|
274
|
+
vmTemplate = undefined;
|
|
275
|
+
} else if (vmChoice !== VM_KEEP) {
|
|
276
|
+
vmTemplate = vmChoice; // an existing template name
|
|
277
|
+
}
|
|
278
|
+
// VM_KEEP leaves vmTemplate at currentConfig.vm_template.
|
|
176
279
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
280
|
+
// Update service configuration. resolveReconfiguredProviderConfig preserves
|
|
281
|
+
// every existing key and carries vm_template forward (ISS-0128 drop guard).
|
|
282
|
+
const newConfig = resolveReconfiguredProviderConfig({
|
|
283
|
+
default_target_node: targetNode,
|
|
284
|
+
lxc_template: lxcTemplate,
|
|
285
|
+
storage,
|
|
286
|
+
vm_template: vmTemplate,
|
|
287
|
+
});
|
|
288
|
+
await updateServiceProviderConfig(service.id, newConfig);
|
|
289
|
+
|
|
290
|
+
celiloOutro(
|
|
291
|
+
`Service '${serviceId}' reconfigured successfully!\n\n` +
|
|
292
|
+
` Target node: ${targetNode}\n` +
|
|
293
|
+
` Storage: ${storage}\n` +
|
|
294
|
+
` LXC template: ${lxcTemplate}\n` +
|
|
295
|
+
` VM template: ${vmTemplate ?? '(none)'}\n\n` +
|
|
296
|
+
`Next steps:\n - Verify: celilo service verify ${serviceId}\n - Deploy: celilo module deploy <module-id>`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
message: `Reconfigured service: ${serviceId}`,
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: `Failed to reconfigure service: ${error instanceof Error ? error.message : String(error)}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
});
|
|
187
310
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Remove a container service
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import { eq } from 'drizzle-orm';
|
|
8
7
|
import { getDb } from '../../db/client';
|
|
9
8
|
import { moduleInfrastructure } from '../../db/schema';
|
|
9
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
10
10
|
import {
|
|
11
11
|
getContainerServiceByServiceId,
|
|
12
12
|
removeContainerService,
|
|
@@ -60,13 +60,16 @@ export async function handleServiceRemove(
|
|
|
60
60
|
console.log('');
|
|
61
61
|
|
|
62
62
|
if (!flags.force) {
|
|
63
|
-
const confirmed = await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const confirmed = await withInterviewSession(() =>
|
|
64
|
+
askConfirm({
|
|
65
|
+
scope: `service:${service.serviceId}`,
|
|
66
|
+
key: 'remove_unassign_modules',
|
|
67
|
+
message: 'Remove service and unassign modules?',
|
|
68
|
+
defaultValue: false,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
67
71
|
|
|
68
|
-
if (
|
|
69
|
-
p.cancel('Operation cancelled');
|
|
72
|
+
if (!confirmed) {
|
|
70
73
|
return { success: false, error: 'Cancelled by user' };
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -74,13 +77,16 @@ export async function handleServiceRemove(
|
|
|
74
77
|
|
|
75
78
|
// Confirm deletion
|
|
76
79
|
if (!flags.force) {
|
|
77
|
-
const confirmed = await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const confirmed = await withInterviewSession(() =>
|
|
81
|
+
askConfirm({
|
|
82
|
+
scope: `service:${service.serviceId}`,
|
|
83
|
+
key: 'remove',
|
|
84
|
+
message: `Remove service '${service.serviceId}' (${service.name})?`,
|
|
85
|
+
defaultValue: false,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
81
88
|
|
|
82
|
-
if (
|
|
83
|
-
p.cancel('Operation cancelled');
|
|
89
|
+
if (!confirmed) {
|
|
84
90
|
return { success: false, error: 'Cancelled by user' };
|
|
85
91
|
}
|
|
86
92
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Re-verify a container service connection
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import { extractTemplateFilename } from '../../api-clients/proxmox';
|
|
7
|
+
import { askConfirm, withInterviewSession } from '../../services/bus-interview';
|
|
8
8
|
import {
|
|
9
9
|
type ProxmoxCredentials,
|
|
10
10
|
getContainerServiceByServiceId,
|
|
@@ -91,15 +91,14 @@ export async function handleServiceVerify(
|
|
|
91
91
|
testResult.message.includes('Template') &&
|
|
92
92
|
testResult.message.includes('not found')
|
|
93
93
|
) {
|
|
94
|
-
const shouldDownload = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
94
|
+
const shouldDownload = await withInterviewSession(() =>
|
|
95
|
+
askConfirm({
|
|
96
|
+
scope: `service:${service.serviceId}`,
|
|
97
|
+
key: 'download_template',
|
|
98
|
+
message: 'Download missing template now?',
|
|
99
|
+
defaultValue: true,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
103
102
|
|
|
104
103
|
if (shouldDownload) {
|
|
105
104
|
try {
|