@celilo/cli 0.1.0
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/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for container service operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { eq } from 'drizzle-orm';
|
|
10
|
+
import { closeDb, createDbClient } from '../db/client';
|
|
11
|
+
import { runMigrations } from '../db/migrate';
|
|
12
|
+
import { containerServices } from '../db/schema';
|
|
13
|
+
import {
|
|
14
|
+
addContainerService,
|
|
15
|
+
getContainerService,
|
|
16
|
+
getContainerServiceByName,
|
|
17
|
+
getServiceCredentials,
|
|
18
|
+
listContainerServices,
|
|
19
|
+
removeContainerService,
|
|
20
|
+
} from './container-service';
|
|
21
|
+
|
|
22
|
+
describe('container-service', () => {
|
|
23
|
+
let testDbPath: string;
|
|
24
|
+
let testDir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
// Create temp directory for test database
|
|
28
|
+
testDir = mkdtempSync(join(tmpdir(), 'celilo-test-'));
|
|
29
|
+
testDbPath = join(testDir, 'test.db');
|
|
30
|
+
|
|
31
|
+
// Set environment variable for database path
|
|
32
|
+
process.env.CELILO_DB_PATH = testDbPath;
|
|
33
|
+
|
|
34
|
+
// Initialize database and run migrations
|
|
35
|
+
await runMigrations(testDbPath);
|
|
36
|
+
|
|
37
|
+
// Create a dummy master key for encryption
|
|
38
|
+
const masterKeyPath = join(testDir, 'master.key');
|
|
39
|
+
process.env.CELILO_MASTER_KEY_PATH = masterKeyPath;
|
|
40
|
+
const fs = await import('node:fs/promises');
|
|
41
|
+
await fs.writeFile(masterKeyPath, 'a'.repeat(64), 'utf8');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
// Close database connection
|
|
46
|
+
closeDb();
|
|
47
|
+
|
|
48
|
+
// Clean up test directory
|
|
49
|
+
if (testDir) {
|
|
50
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Clear environment variables
|
|
54
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
55
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('addContainerService', () => {
|
|
59
|
+
it('creates a Proxmox service', async () => {
|
|
60
|
+
const service = await addContainerService({
|
|
61
|
+
name: 'Test Proxmox',
|
|
62
|
+
providerName: 'proxmox',
|
|
63
|
+
zones: ['dmz', 'app', 'secure'],
|
|
64
|
+
providerConfig: {
|
|
65
|
+
default_target_node: 'pve',
|
|
66
|
+
lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
|
|
67
|
+
storage: 'local-lvm',
|
|
68
|
+
},
|
|
69
|
+
apiCredentials: {
|
|
70
|
+
api_url: 'https://proxmox.local:8006/api2/json',
|
|
71
|
+
api_token_id: 'root@pam!celilo',
|
|
72
|
+
api_token_secret: 'test-token-secret',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(service.id).toBeDefined();
|
|
77
|
+
expect(service.name).toBe('Test Proxmox');
|
|
78
|
+
expect(service.providerName).toBe('proxmox');
|
|
79
|
+
expect(service.zones).toEqual(['dmz', 'app', 'secure']);
|
|
80
|
+
expect(service.apiCredentialsEncrypted).toBeDefined();
|
|
81
|
+
expect(service.createdAt).toBeInstanceOf(Date);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('creates a Digital Ocean service', async () => {
|
|
85
|
+
const service = await addContainerService({
|
|
86
|
+
name: 'Digital Ocean NYC3',
|
|
87
|
+
providerName: 'digitalocean',
|
|
88
|
+
zones: ['external'],
|
|
89
|
+
providerConfig: {
|
|
90
|
+
region: 'nyc3',
|
|
91
|
+
default_size: 's-1vcpu-1gb',
|
|
92
|
+
default_image: 'ubuntu-22-04-x64',
|
|
93
|
+
},
|
|
94
|
+
apiCredentials: {
|
|
95
|
+
api_token: 'dop_test_token_12345',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(service.id).toBeDefined();
|
|
100
|
+
expect(service.name).toBe('Digital Ocean NYC3');
|
|
101
|
+
expect(service.providerName).toBe('digitalocean');
|
|
102
|
+
expect(service.zones).toEqual(['external']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('encrypts API credentials', async () => {
|
|
106
|
+
const service = await addContainerService({
|
|
107
|
+
name: 'Test Service',
|
|
108
|
+
providerName: 'proxmox',
|
|
109
|
+
zones: ['dmz'],
|
|
110
|
+
providerConfig: {},
|
|
111
|
+
apiCredentials: {
|
|
112
|
+
api_url: 'https://test.local',
|
|
113
|
+
api_token_id: 'root@pam!celilo',
|
|
114
|
+
api_token_secret: 'secret-token-123',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Verify credentials are encrypted in database
|
|
119
|
+
const db = createDbClient({ path: testDbPath });
|
|
120
|
+
const dbRecord = await db
|
|
121
|
+
.select()
|
|
122
|
+
.from(containerServices)
|
|
123
|
+
.where(eq(containerServices.id, service.id))
|
|
124
|
+
.get();
|
|
125
|
+
|
|
126
|
+
expect(dbRecord?.apiCredentialsEncrypted).toBeDefined();
|
|
127
|
+
expect(dbRecord?.apiCredentialsEncrypted).not.toContain('secret-token-123');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('getContainerService', () => {
|
|
132
|
+
it('retrieves service by ID', async () => {
|
|
133
|
+
const created = await addContainerService({
|
|
134
|
+
name: 'Test Service',
|
|
135
|
+
providerName: 'proxmox',
|
|
136
|
+
zones: ['dmz'],
|
|
137
|
+
providerConfig: {},
|
|
138
|
+
apiCredentials: { api_url: 'https://test' },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const retrieved = await getContainerService(created.id);
|
|
142
|
+
|
|
143
|
+
expect(retrieved).toBeDefined();
|
|
144
|
+
expect(retrieved?.id).toBe(created.id);
|
|
145
|
+
expect(retrieved?.name).toBe('Test Service');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns null for non-existent service', async () => {
|
|
149
|
+
const result = await getContainerService('non-existent-id');
|
|
150
|
+
expect(result).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('getContainerServiceByName', () => {
|
|
155
|
+
it('retrieves service by name', async () => {
|
|
156
|
+
await addContainerService({
|
|
157
|
+
name: 'Unique Service Name',
|
|
158
|
+
providerName: 'proxmox',
|
|
159
|
+
zones: ['dmz'],
|
|
160
|
+
providerConfig: {},
|
|
161
|
+
apiCredentials: { api_url: 'https://test' },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const retrieved = await getContainerServiceByName('Unique Service Name');
|
|
165
|
+
|
|
166
|
+
expect(retrieved).toBeDefined();
|
|
167
|
+
expect(retrieved?.name).toBe('Unique Service Name');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('returns null for non-existent service name', async () => {
|
|
171
|
+
const result = await getContainerServiceByName('Does Not Exist');
|
|
172
|
+
expect(result).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('listContainerServices', () => {
|
|
177
|
+
it('returns empty array when no services', async () => {
|
|
178
|
+
const services = await listContainerServices();
|
|
179
|
+
expect(services).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('lists all services without filters', async () => {
|
|
183
|
+
await addContainerService({
|
|
184
|
+
name: 'Service 1',
|
|
185
|
+
providerName: 'proxmox',
|
|
186
|
+
zones: ['dmz'],
|
|
187
|
+
providerConfig: {},
|
|
188
|
+
apiCredentials: { api_url: 'https://test1' },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await addContainerService({
|
|
192
|
+
name: 'Service 2',
|
|
193
|
+
providerName: 'digitalocean',
|
|
194
|
+
zones: ['external'],
|
|
195
|
+
providerConfig: {},
|
|
196
|
+
apiCredentials: { api_token: 'test2' },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const services = await listContainerServices();
|
|
200
|
+
expect(services).toHaveLength(2);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('filters services by zone', async () => {
|
|
204
|
+
await addContainerService({
|
|
205
|
+
name: 'Proxmox Internal',
|
|
206
|
+
providerName: 'proxmox',
|
|
207
|
+
zones: ['dmz', 'app', 'secure'],
|
|
208
|
+
providerConfig: {},
|
|
209
|
+
apiCredentials: { api_url: 'https://test1' },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await addContainerService({
|
|
213
|
+
name: 'Digital Ocean External',
|
|
214
|
+
providerName: 'digitalocean',
|
|
215
|
+
zones: ['external'],
|
|
216
|
+
providerConfig: {},
|
|
217
|
+
apiCredentials: { api_token: 'test2' },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const externalServices = await listContainerServices({
|
|
221
|
+
zones: ['external'],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(externalServices).toHaveLength(1);
|
|
225
|
+
expect(externalServices[0].name).toBe('Digital Ocean External');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns services matching any zone in filter', async () => {
|
|
229
|
+
await addContainerService({
|
|
230
|
+
name: 'Multi-Zone Service',
|
|
231
|
+
providerName: 'proxmox',
|
|
232
|
+
zones: ['dmz', 'app'],
|
|
233
|
+
providerConfig: {},
|
|
234
|
+
apiCredentials: { api_url: 'https://test' },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const dmzServices = await listContainerServices({ zones: ['dmz'] });
|
|
238
|
+
const appServices = await listContainerServices({ zones: ['app'] });
|
|
239
|
+
|
|
240
|
+
expect(dmzServices).toHaveLength(1);
|
|
241
|
+
expect(appServices).toHaveLength(1);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('getServiceCredentials', () => {
|
|
246
|
+
it('decrypts and returns service credentials', async () => {
|
|
247
|
+
const service = await addContainerService({
|
|
248
|
+
name: 'Test Service',
|
|
249
|
+
providerName: 'proxmox',
|
|
250
|
+
zones: ['dmz'],
|
|
251
|
+
providerConfig: {},
|
|
252
|
+
apiCredentials: {
|
|
253
|
+
api_url: 'https://proxmox.local:8006',
|
|
254
|
+
api_token_id: 'root@pam!celilo',
|
|
255
|
+
api_token_secret: 'secret-token-123',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const credentials = await getServiceCredentials(service.id);
|
|
260
|
+
|
|
261
|
+
expect((credentials as { api_url: string }).api_url).toBe('https://proxmox.local:8006');
|
|
262
|
+
expect((credentials as { api_token_id: string }).api_token_id).toBe('root@pam!celilo');
|
|
263
|
+
expect((credentials as { api_token_secret: string }).api_token_secret).toBe(
|
|
264
|
+
'secret-token-123',
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('throws error for non-existent service', async () => {
|
|
269
|
+
await expect(getServiceCredentials('non-existent-id')).rejects.toThrow(
|
|
270
|
+
/Container service not found/,
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('removeContainerService', () => {
|
|
276
|
+
it('deletes service from database', async () => {
|
|
277
|
+
const service = await addContainerService({
|
|
278
|
+
name: 'Service To Delete',
|
|
279
|
+
providerName: 'proxmox',
|
|
280
|
+
zones: ['dmz'],
|
|
281
|
+
providerConfig: {},
|
|
282
|
+
apiCredentials: { api_url: 'https://test' },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await removeContainerService(service.id);
|
|
286
|
+
|
|
287
|
+
const retrieved = await getContainerService(service.id);
|
|
288
|
+
expect(retrieved).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not throw when removing non-existent service', async () => {
|
|
292
|
+
// Should complete without error
|
|
293
|
+
await removeContainerService('non-existent-id');
|
|
294
|
+
// If we get here, no error was thrown
|
|
295
|
+
expect(true).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { getDb } from '../db/client';
|
|
5
|
+
import { type NetworkZone, containerServices } from '../db/schema';
|
|
6
|
+
import { decryptSecret, encryptSecret } from '../secrets/encryption';
|
|
7
|
+
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
8
|
+
import type { ContainerService, TestResult } from '../types/infrastructure';
|
|
9
|
+
import { EncryptionEnvelopeSchema, parseJsonWithValidation } from '../validation/schemas';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Zod schemas for provider-specific credentials
|
|
13
|
+
* Validates external data from database storage (Rule 3.7)
|
|
14
|
+
*/
|
|
15
|
+
const ProxmoxCredentialsSchema = z.object({
|
|
16
|
+
api_url: z.string().url(),
|
|
17
|
+
api_token_id: z.string().min(1),
|
|
18
|
+
api_token_secret: z.string().min(1),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const DigitalOceanCredentialsSchema = z.object({
|
|
22
|
+
api_token: z.string().min(1),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type ProxmoxCredentials = z.infer<typeof ProxmoxCredentialsSchema>;
|
|
26
|
+
export type DigitalOceanCredentials = z.infer<typeof DigitalOceanCredentialsSchema>;
|
|
27
|
+
export type ServiceCredentials = ProxmoxCredentials | DigitalOceanCredentials;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Container service filters
|
|
31
|
+
*/
|
|
32
|
+
export interface ContainerServiceFilters {
|
|
33
|
+
zones?: NetworkZone[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a service ID from a human-readable name
|
|
38
|
+
* Converts to kebab-case, removes special characters
|
|
39
|
+
*/
|
|
40
|
+
function generateServiceId(name: string): string {
|
|
41
|
+
return name
|
|
42
|
+
.toLowerCase()
|
|
43
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
44
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add a new container service
|
|
49
|
+
*/
|
|
50
|
+
export async function addContainerService(
|
|
51
|
+
service: Omit<
|
|
52
|
+
ContainerService,
|
|
53
|
+
| 'id'
|
|
54
|
+
| 'serviceId'
|
|
55
|
+
| 'createdAt'
|
|
56
|
+
| 'updatedAt'
|
|
57
|
+
| 'apiCredentialsEncrypted'
|
|
58
|
+
| 'verified'
|
|
59
|
+
| 'verifiedAt'
|
|
60
|
+
| 'verificationError'
|
|
61
|
+
> & {
|
|
62
|
+
apiCredentials: Record<string, unknown>;
|
|
63
|
+
},
|
|
64
|
+
): Promise<ContainerService> {
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const id = randomUUID();
|
|
67
|
+
const serviceId = generateServiceId(service.name);
|
|
68
|
+
const now = new Date();
|
|
69
|
+
|
|
70
|
+
// Check if service ID already exists
|
|
71
|
+
const existing = await db
|
|
72
|
+
.select()
|
|
73
|
+
.from(containerServices)
|
|
74
|
+
.where(eq(containerServices.serviceId, serviceId))
|
|
75
|
+
.limit(1);
|
|
76
|
+
|
|
77
|
+
if (existing.length > 0) {
|
|
78
|
+
throw new Error(`Service ID '${serviceId}' already exists. Please choose a different name.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Encrypt API credentials
|
|
82
|
+
const masterKey = await getOrCreateMasterKey();
|
|
83
|
+
const encrypted = encryptSecret(JSON.stringify(service.apiCredentials), masterKey);
|
|
84
|
+
|
|
85
|
+
const values = {
|
|
86
|
+
id,
|
|
87
|
+
serviceId,
|
|
88
|
+
name: service.name,
|
|
89
|
+
providerName: service.providerName,
|
|
90
|
+
zones: service.zones, // Drizzle auto-stringifies with mode: 'json'
|
|
91
|
+
apiCredentialsEncrypted: JSON.stringify(encrypted),
|
|
92
|
+
providerConfig: service.providerConfig, // Drizzle auto-stringifies with mode: 'json'
|
|
93
|
+
verified: false,
|
|
94
|
+
verifiedAt: null,
|
|
95
|
+
verificationError: null,
|
|
96
|
+
createdAt: now,
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await db.insert(containerServices).values(values);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
serviceId,
|
|
105
|
+
name: service.name,
|
|
106
|
+
providerName: service.providerName,
|
|
107
|
+
zones: service.zones,
|
|
108
|
+
apiCredentialsEncrypted: JSON.stringify(encrypted),
|
|
109
|
+
providerConfig: service.providerConfig,
|
|
110
|
+
verified: false,
|
|
111
|
+
verifiedAt: null,
|
|
112
|
+
verificationError: null,
|
|
113
|
+
createdAt: now,
|
|
114
|
+
updatedAt: now,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get container service by ID
|
|
120
|
+
*/
|
|
121
|
+
export async function getContainerService(id: string): Promise<ContainerService | null> {
|
|
122
|
+
const db = getDb();
|
|
123
|
+
|
|
124
|
+
const result = await db
|
|
125
|
+
.select()
|
|
126
|
+
.from(containerServices)
|
|
127
|
+
.where(eq(containerServices.id, id))
|
|
128
|
+
.limit(1);
|
|
129
|
+
|
|
130
|
+
if (result.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const row = result[0];
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
id: row.id,
|
|
138
|
+
serviceId: row.serviceId,
|
|
139
|
+
name: row.name,
|
|
140
|
+
providerName: row.providerName as ContainerService['providerName'],
|
|
141
|
+
zones: row.zones, // Drizzle auto-parses with mode: 'json'
|
|
142
|
+
apiCredentialsEncrypted: row.apiCredentialsEncrypted,
|
|
143
|
+
providerConfig: row.providerConfig, // Drizzle auto-parses with mode: 'json'
|
|
144
|
+
verified: Boolean(row.verified),
|
|
145
|
+
verifiedAt: row.verifiedAt ? new Date(row.verifiedAt) : null,
|
|
146
|
+
verificationError: row.verificationError,
|
|
147
|
+
createdAt: new Date(row.createdAt),
|
|
148
|
+
updatedAt: new Date(row.updatedAt),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get container service by service ID (user-facing identifier)
|
|
154
|
+
*/
|
|
155
|
+
export async function getContainerServiceByServiceId(
|
|
156
|
+
serviceId: string,
|
|
157
|
+
): Promise<ContainerService | null> {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
|
|
160
|
+
const result = await db
|
|
161
|
+
.select()
|
|
162
|
+
.from(containerServices)
|
|
163
|
+
.where(eq(containerServices.serviceId, serviceId))
|
|
164
|
+
.limit(1);
|
|
165
|
+
|
|
166
|
+
if (result.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const row = result[0];
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id: row.id,
|
|
174
|
+
serviceId: row.serviceId,
|
|
175
|
+
name: row.name,
|
|
176
|
+
providerName: row.providerName as ContainerService['providerName'],
|
|
177
|
+
zones: row.zones, // Drizzle auto-parses with mode: 'json'
|
|
178
|
+
apiCredentialsEncrypted: row.apiCredentialsEncrypted,
|
|
179
|
+
providerConfig: row.providerConfig, // Drizzle auto-parses with mode: 'json'
|
|
180
|
+
verified: Boolean(row.verified),
|
|
181
|
+
verifiedAt: row.verifiedAt ? new Date(row.verifiedAt) : null,
|
|
182
|
+
verificationError: row.verificationError,
|
|
183
|
+
createdAt: new Date(row.createdAt),
|
|
184
|
+
updatedAt: new Date(row.updatedAt),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get container service by name
|
|
190
|
+
*/
|
|
191
|
+
export async function getContainerServiceByName(name: string): Promise<ContainerService | null> {
|
|
192
|
+
const db = getDb();
|
|
193
|
+
|
|
194
|
+
const result = await db
|
|
195
|
+
.select()
|
|
196
|
+
.from(containerServices)
|
|
197
|
+
.where(eq(containerServices.name, name))
|
|
198
|
+
.limit(1);
|
|
199
|
+
|
|
200
|
+
if (result.length === 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const row = result[0];
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: row.id,
|
|
208
|
+
serviceId: row.serviceId,
|
|
209
|
+
name: row.name,
|
|
210
|
+
providerName: row.providerName as ContainerService['providerName'],
|
|
211
|
+
zones: row.zones, // Drizzle auto-parses with mode: 'json'
|
|
212
|
+
apiCredentialsEncrypted: row.apiCredentialsEncrypted,
|
|
213
|
+
providerConfig: row.providerConfig, // Drizzle auto-parses with mode: 'json'
|
|
214
|
+
verified: Boolean(row.verified),
|
|
215
|
+
verifiedAt: row.verifiedAt ? new Date(row.verifiedAt) : null,
|
|
216
|
+
verificationError: row.verificationError,
|
|
217
|
+
createdAt: new Date(row.createdAt),
|
|
218
|
+
updatedAt: new Date(row.updatedAt),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get decrypted API credentials for a service
|
|
224
|
+
* Returns validated, typed credentials based on provider (Rule 3.7)
|
|
225
|
+
*/
|
|
226
|
+
export async function getServiceCredentials(serviceId: string): Promise<ServiceCredentials> {
|
|
227
|
+
const service = await getContainerService(serviceId);
|
|
228
|
+
if (!service) {
|
|
229
|
+
throw new Error(`Container service not found: ${serviceId}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const masterKey = await getOrCreateMasterKey();
|
|
233
|
+
const encrypted = parseJsonWithValidation(
|
|
234
|
+
service.apiCredentialsEncrypted,
|
|
235
|
+
EncryptionEnvelopeSchema,
|
|
236
|
+
'service credentials encryption envelope',
|
|
237
|
+
);
|
|
238
|
+
const decrypted = decryptSecret(encrypted, masterKey);
|
|
239
|
+
const parsed = JSON.parse(decrypted);
|
|
240
|
+
|
|
241
|
+
// Validate based on provider type
|
|
242
|
+
if (service.providerName === 'proxmox') {
|
|
243
|
+
return ProxmoxCredentialsSchema.parse(parsed);
|
|
244
|
+
}
|
|
245
|
+
if (service.providerName === 'digitalocean') {
|
|
246
|
+
return DigitalOceanCredentialsSchema.parse(parsed);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Unsupported provider for credential validation: ${service.providerName}. Supported providers: proxmox, digitalocean`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* List container services with optional filters
|
|
256
|
+
*/
|
|
257
|
+
export async function listContainerServices(
|
|
258
|
+
filters?: ContainerServiceFilters,
|
|
259
|
+
): Promise<ContainerService[]> {
|
|
260
|
+
const db = getDb();
|
|
261
|
+
|
|
262
|
+
const results = await db.select().from(containerServices);
|
|
263
|
+
|
|
264
|
+
let services = results.map((row) => ({
|
|
265
|
+
id: row.id,
|
|
266
|
+
serviceId: row.serviceId,
|
|
267
|
+
name: row.name,
|
|
268
|
+
providerName: row.providerName as ContainerService['providerName'],
|
|
269
|
+
zones: row.zones, // Drizzle auto-parses with mode: 'json'
|
|
270
|
+
apiCredentialsEncrypted: row.apiCredentialsEncrypted,
|
|
271
|
+
providerConfig: row.providerConfig, // Drizzle auto-parses with mode: 'json'
|
|
272
|
+
verified: Boolean(row.verified),
|
|
273
|
+
verifiedAt: row.verifiedAt ? new Date(row.verifiedAt) : null,
|
|
274
|
+
verificationError: row.verificationError,
|
|
275
|
+
createdAt: new Date(row.createdAt),
|
|
276
|
+
updatedAt: new Date(row.updatedAt),
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Apply zone filter
|
|
280
|
+
if (filters?.zones && filters.zones.length > 0) {
|
|
281
|
+
services = services.filter((service) =>
|
|
282
|
+
filters.zones?.some((zone) => service.zones.includes(zone)),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return services;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Remove a container service
|
|
291
|
+
*/
|
|
292
|
+
/**
|
|
293
|
+
* Update provider configuration for a container service
|
|
294
|
+
*/
|
|
295
|
+
export async function updateServiceProviderConfig(
|
|
296
|
+
id: string,
|
|
297
|
+
providerConfig: Record<string, unknown>,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const db = getDb();
|
|
300
|
+
await db
|
|
301
|
+
.update(containerServices)
|
|
302
|
+
.set({
|
|
303
|
+
providerConfig,
|
|
304
|
+
updatedAt: new Date(),
|
|
305
|
+
})
|
|
306
|
+
.where(eq(containerServices.id, id));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function removeContainerService(id: string): Promise<void> {
|
|
310
|
+
const db = getDb();
|
|
311
|
+
|
|
312
|
+
await db.delete(containerServices).where(eq(containerServices.id, id));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Test connection to a container service
|
|
317
|
+
* Validates credentials and connectivity for the provider
|
|
318
|
+
*/
|
|
319
|
+
export async function testConnection(service: ContainerService): Promise<TestResult> {
|
|
320
|
+
const credentials = await getServiceCredentials(service.id);
|
|
321
|
+
|
|
322
|
+
if (service.providerName === 'proxmox') {
|
|
323
|
+
const { testProxmoxConnection } = await import('../api-clients/proxmox');
|
|
324
|
+
const proxmoxCreds = credentials as ProxmoxCredentials;
|
|
325
|
+
|
|
326
|
+
// Cast providerConfig to expected shape
|
|
327
|
+
const providerConfig = service.providerConfig as {
|
|
328
|
+
default_target_node: string;
|
|
329
|
+
lxc_template: string;
|
|
330
|
+
storage: string;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return testProxmoxConnection(
|
|
334
|
+
{
|
|
335
|
+
api_url: proxmoxCreds.api_url,
|
|
336
|
+
api_token_id: proxmoxCreds.api_token_id,
|
|
337
|
+
api_token_secret: proxmoxCreds.api_token_secret,
|
|
338
|
+
},
|
|
339
|
+
providerConfig,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (service.providerName === 'digitalocean') {
|
|
344
|
+
const { testDigitalOceanConnection } = await import('../api-clients/digitalocean');
|
|
345
|
+
const doCreds = credentials as DigitalOceanCredentials;
|
|
346
|
+
return testDigitalOceanConnection({
|
|
347
|
+
api_token: doCreds.api_token,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
message: `Connection test not implemented for provider: ${service.providerName}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Update verification status for a container service
|
|
359
|
+
*/
|
|
360
|
+
export async function updateVerificationStatus(
|
|
361
|
+
serviceId: string,
|
|
362
|
+
testResult: TestResult,
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const db = getDb();
|
|
365
|
+
const now = new Date();
|
|
366
|
+
|
|
367
|
+
await db
|
|
368
|
+
.update(containerServices)
|
|
369
|
+
.set({
|
|
370
|
+
verified: testResult.success,
|
|
371
|
+
verifiedAt: testResult.success ? now : null,
|
|
372
|
+
verificationError: testResult.success ? null : testResult.message || 'Connection test failed',
|
|
373
|
+
updatedAt: now,
|
|
374
|
+
})
|
|
375
|
+
.where(eq(containerServices.id, serviceId));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Verify a container service and update its status
|
|
380
|
+
* Returns the updated service
|
|
381
|
+
*/
|
|
382
|
+
export async function verifyContainerService(serviceId: string): Promise<{
|
|
383
|
+
service: ContainerService;
|
|
384
|
+
testResult: TestResult;
|
|
385
|
+
}> {
|
|
386
|
+
const service = await getContainerService(serviceId);
|
|
387
|
+
if (!service) {
|
|
388
|
+
throw new Error(`Container service not found: ${serviceId}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const testResult = await testConnection(service);
|
|
392
|
+
await updateVerificationStatus(serviceId, testResult);
|
|
393
|
+
|
|
394
|
+
// Fetch updated service
|
|
395
|
+
const updatedService = await getContainerService(serviceId);
|
|
396
|
+
if (!updatedService) {
|
|
397
|
+
throw new Error(`Failed to fetch updated service: ${serviceId}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { service: updatedService, testResult };
|
|
401
|
+
}
|