@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,200 @@
|
|
|
1
|
+
import type { ModuleManifest, VariableDeclare } from '../manifest/schema';
|
|
2
|
+
import type { ResolutionContext } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get nested property from object using dot notation
|
|
6
|
+
*
|
|
7
|
+
* Policy function - pure navigation
|
|
8
|
+
*
|
|
9
|
+
* @param obj - Object to navigate
|
|
10
|
+
* @param path - Dot-separated path (e.g., "server.ip.primary")
|
|
11
|
+
* @returns Value at path or undefined if not found
|
|
12
|
+
*/
|
|
13
|
+
function getNestedProperty(obj: Record<string, unknown>, path: string): unknown {
|
|
14
|
+
const parts = path.split('.');
|
|
15
|
+
let current: unknown = obj;
|
|
16
|
+
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (current && typeof current === 'object' && part in (current as Record<string, unknown>)) {
|
|
19
|
+
current = (current as Record<string, unknown>)[part];
|
|
20
|
+
} else {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve declarative variable derivation from manifest
|
|
30
|
+
*
|
|
31
|
+
* Policy function - pure template substitution
|
|
32
|
+
*
|
|
33
|
+
* Supports the following template patterns:
|
|
34
|
+
* - $system:key - System config value (e.g., $system:primary_domain)
|
|
35
|
+
* - {variable} - Module's own variable (e.g., {hostname})
|
|
36
|
+
* - $capability:name.path - Capability data (e.g., $capability:dns_registrar.primary_domain)
|
|
37
|
+
*
|
|
38
|
+
* @param variable - Variable declaration with derive_from field
|
|
39
|
+
* @param context - Resolution context (selfConfig, systemConfig, etc.)
|
|
40
|
+
* @returns Derived value or undefined if dependencies missing
|
|
41
|
+
* @throws Error if required dependencies are missing
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Pattern that matches any unresolved variable reference.
|
|
45
|
+
* Used to detect whether another resolution pass is needed.
|
|
46
|
+
*/
|
|
47
|
+
const UNRESOLVED_PATTERN = /\$\{?(?:system|self|capability|secret|system_secret):/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run one pass of variable substitution on a string.
|
|
51
|
+
*/
|
|
52
|
+
function substituteVariables(
|
|
53
|
+
input: string,
|
|
54
|
+
variableName: string,
|
|
55
|
+
context: ResolutionContext,
|
|
56
|
+
): string {
|
|
57
|
+
let result = input;
|
|
58
|
+
|
|
59
|
+
// Replace $system:key patterns (both $system:key and ${system:key} forms)
|
|
60
|
+
result = result.replace(/\$\{?system:([a-zA-Z0-9_.]+)\}?/g, (_match, key) => {
|
|
61
|
+
const value = context.systemConfig[key];
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
throw new Error(`Missing system config: ${key} (required by variable '${variableName}')`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Replace {variable_name} patterns
|
|
69
|
+
result = result.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, varName) => {
|
|
70
|
+
const value = context.selfConfig[varName];
|
|
71
|
+
if (value === undefined) {
|
|
72
|
+
throw new Error(`Missing variable: ${varName} (required by variable '${variableName}')`);
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Replace $capability:name.path patterns
|
|
78
|
+
result = result.replace(
|
|
79
|
+
/\$capability:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_.]+)/g,
|
|
80
|
+
(_match, capName, path) => {
|
|
81
|
+
const capData = context.capabilities[capName];
|
|
82
|
+
if (!capData) {
|
|
83
|
+
throw new Error(`Missing capability: ${capName} (required by variable '${variableName}')`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const value = getNestedProperty(capData, path);
|
|
87
|
+
if (value === undefined) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Missing capability field: ${capName}.${path} (required by variable '${variableName}')`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return String(value);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveDeclarativeDerivation(
|
|
100
|
+
variable: VariableDeclare,
|
|
101
|
+
context: ResolutionContext,
|
|
102
|
+
): string | undefined {
|
|
103
|
+
if (!variable.derive_from) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const template = variable.derive_from;
|
|
108
|
+
|
|
109
|
+
// $machine: derivations are handled by the config interview, not template resolution
|
|
110
|
+
if (template.startsWith('$machine:')) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Resolve variables iteratively — capability values may contain $system: or
|
|
115
|
+
// other references that need a second pass to fully resolve.
|
|
116
|
+
let result = template;
|
|
117
|
+
const maxPasses = 5;
|
|
118
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
119
|
+
const resolved = substituteVariables(result, variable.name, context);
|
|
120
|
+
if (resolved === result) break; // Stable — no more substitutions possible
|
|
121
|
+
result = resolved;
|
|
122
|
+
if (!UNRESOLVED_PATTERN.test(result)) break; // Fully resolved
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Apply all declarative derivations from manifest
|
|
130
|
+
*
|
|
131
|
+
* Planning function - processes variable declarations in order
|
|
132
|
+
*
|
|
133
|
+
* Rules:
|
|
134
|
+
* 1. User-provided values always take precedence (not overwritten)
|
|
135
|
+
* 2. Variables are resolved in declaration order
|
|
136
|
+
* 3. Only works for type: string variables
|
|
137
|
+
* 4. If derivation fails for optional variable, silently skip
|
|
138
|
+
* 5. If derivation fails for required variable, throw error
|
|
139
|
+
*
|
|
140
|
+
* @param manifest - Module manifest with variable declarations
|
|
141
|
+
* @param context - Resolution context (will be mutated with derived values)
|
|
142
|
+
*/
|
|
143
|
+
export function applyDeclarativeDerivations(
|
|
144
|
+
manifest: ModuleManifest,
|
|
145
|
+
context: ResolutionContext,
|
|
146
|
+
): void {
|
|
147
|
+
const variables = manifest.variables?.owns ?? [];
|
|
148
|
+
|
|
149
|
+
for (const variable of variables) {
|
|
150
|
+
// Re-derive capability-sourced and infrastructure-sourced variables every time,
|
|
151
|
+
// since the upstream data may have changed. User-provided values take precedence
|
|
152
|
+
// only for user-sourced variables.
|
|
153
|
+
const shouldRederive = variable.source === 'capability' || variable.source === 'infrastructure';
|
|
154
|
+
if (!shouldRederive && context.selfConfig[variable.name] !== undefined) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Skip if no derivation defined
|
|
159
|
+
if (!variable.derive_from) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Declarative derivation only resolves string template expressions.
|
|
164
|
+
// Non-string types (arrays, objects) with derive_from are handled by
|
|
165
|
+
// the config interview system instead.
|
|
166
|
+
if (variable.type !== 'string') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const derived = resolveDeclarativeDerivation(variable, context);
|
|
172
|
+
if (derived !== undefined) {
|
|
173
|
+
// Don't overwrite with a value that still contains unresolved
|
|
174
|
+
// template references ($self:, $system:, $capability:). This
|
|
175
|
+
// happens when capability data contains template strings that
|
|
176
|
+
// can't be fully resolved in the consuming module's context —
|
|
177
|
+
// e.g. $capability:dns_registrar.primary_domain resolves to
|
|
178
|
+
// namecheap's "$self:primary_domain", but $self: in that
|
|
179
|
+
// context is namecheap, not the consumer.
|
|
180
|
+
const hasUnresolved =
|
|
181
|
+
derived.includes('$self:') ||
|
|
182
|
+
derived.includes('$system:') ||
|
|
183
|
+
derived.includes('$capability:') ||
|
|
184
|
+
derived.includes('$secret:');
|
|
185
|
+
if (hasUnresolved && context.selfConfig[variable.name] !== undefined) {
|
|
186
|
+
// Keep the existing (user-set or previously-resolved) value
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
context.selfConfig[variable.name] = derived;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// If derivation fails due to missing dependencies, handle based on required flag
|
|
193
|
+
if (variable.required) {
|
|
194
|
+
// Required variable - propagate error
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
// Optional variable - silently skip (will use default or remain unset)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { hasVariables, isValidVariableFormat, parseVariables } from './parser';
|
|
3
|
+
|
|
4
|
+
describe('parseVariables', () => {
|
|
5
|
+
test('should parse self variables', () => {
|
|
6
|
+
const content = 'ip: $self:container_ip';
|
|
7
|
+
const variables = parseVariables(content);
|
|
8
|
+
|
|
9
|
+
expect(variables).toHaveLength(1);
|
|
10
|
+
expect(variables[0]).toEqual({
|
|
11
|
+
type: 'self',
|
|
12
|
+
path: 'container_ip',
|
|
13
|
+
raw: '$self:container_ip',
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should parse system variables', () => {
|
|
18
|
+
const content = 'server: $system:management.ip';
|
|
19
|
+
const variables = parseVariables(content);
|
|
20
|
+
|
|
21
|
+
expect(variables).toHaveLength(1);
|
|
22
|
+
expect(variables[0]).toEqual({
|
|
23
|
+
type: 'system',
|
|
24
|
+
path: 'management.ip',
|
|
25
|
+
raw: '$system:management.ip',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should parse secret variables', () => {
|
|
30
|
+
const content = 'key: $secret:api_key';
|
|
31
|
+
const variables = parseVariables(content);
|
|
32
|
+
|
|
33
|
+
expect(variables).toHaveLength(1);
|
|
34
|
+
expect(variables[0]).toEqual({
|
|
35
|
+
type: 'secret',
|
|
36
|
+
path: 'api_key',
|
|
37
|
+
raw: '$secret:api_key',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should parse capability variables', () => {
|
|
42
|
+
const content = 'dns: $capability:dns_external.nameserver';
|
|
43
|
+
const variables = parseVariables(content);
|
|
44
|
+
|
|
45
|
+
expect(variables).toHaveLength(1);
|
|
46
|
+
expect(variables[0]).toEqual({
|
|
47
|
+
type: 'capability',
|
|
48
|
+
path: 'dns_external.nameserver',
|
|
49
|
+
raw: '$capability:dns_external.nameserver',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should parse multiple variables in same content', () => {
|
|
54
|
+
const content = `
|
|
55
|
+
ip: $self:container_ip
|
|
56
|
+
dns: $capability:dns_external.nameserver
|
|
57
|
+
key: $secret:api_key
|
|
58
|
+
`;
|
|
59
|
+
const variables = parseVariables(content);
|
|
60
|
+
|
|
61
|
+
expect(variables).toHaveLength(3);
|
|
62
|
+
expect(variables[0]?.type).toBe('self');
|
|
63
|
+
expect(variables[1]?.type).toBe('capability');
|
|
64
|
+
expect(variables[2]?.type).toBe('secret');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should handle nested paths in capability variables', () => {
|
|
68
|
+
const content = '$capability:dns_external.config.primary.nameserver';
|
|
69
|
+
const variables = parseVariables(content);
|
|
70
|
+
|
|
71
|
+
expect(variables).toHaveLength(1);
|
|
72
|
+
expect(variables[0]?.path).toBe('dns_external.config.primary.nameserver');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should handle underscores and numbers in paths', () => {
|
|
76
|
+
const content = '$self:vlan_id_10';
|
|
77
|
+
const variables = parseVariables(content);
|
|
78
|
+
|
|
79
|
+
expect(variables).toHaveLength(1);
|
|
80
|
+
expect(variables[0]?.path).toBe('vlan_id_10');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should not parse paths starting with numbers', () => {
|
|
84
|
+
const content = '$self:123invalid';
|
|
85
|
+
const variables = parseVariables(content);
|
|
86
|
+
|
|
87
|
+
expect(variables).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should return empty array for content without variables', () => {
|
|
91
|
+
const content = 'no variables here';
|
|
92
|
+
const variables = parseVariables(content);
|
|
93
|
+
|
|
94
|
+
expect(variables).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should handle variables in complex template', () => {
|
|
98
|
+
const content = `
|
|
99
|
+
resource "proxmox_lxc" "homebridge" {
|
|
100
|
+
hostname = "$self:hostname"
|
|
101
|
+
cores = $self:cores
|
|
102
|
+
memory = $self:memory
|
|
103
|
+
network {
|
|
104
|
+
ip = "$self:container_ip/24"
|
|
105
|
+
gw = "$system:management.ip"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
const variables = parseVariables(content);
|
|
110
|
+
|
|
111
|
+
expect(variables.length).toBeGreaterThan(3);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should parse braced variables for concatenation', () => {
|
|
115
|
+
const content = 'size = "${self:disk}G"';
|
|
116
|
+
const variables = parseVariables(content);
|
|
117
|
+
|
|
118
|
+
expect(variables).toHaveLength(1);
|
|
119
|
+
expect(variables[0]).toEqual({
|
|
120
|
+
type: 'self',
|
|
121
|
+
path: 'disk',
|
|
122
|
+
raw: '${self:disk}',
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should parse multiple braced variables', () => {
|
|
127
|
+
const content = 'url = "${system:protocol}://${system:host}:${system:port}"';
|
|
128
|
+
const variables = parseVariables(content);
|
|
129
|
+
|
|
130
|
+
expect(variables).toHaveLength(3);
|
|
131
|
+
expect(variables[0]?.raw).toBe('${system:protocol}');
|
|
132
|
+
expect(variables[1]?.raw).toBe('${system:host}');
|
|
133
|
+
expect(variables[2]?.raw).toBe('${system:port}');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should handle mix of braced and simple variables', () => {
|
|
137
|
+
const content = 'size = "${self:disk}G" and vmid = $self:vmid';
|
|
138
|
+
const variables = parseVariables(content);
|
|
139
|
+
|
|
140
|
+
expect(variables).toHaveLength(2);
|
|
141
|
+
expect(variables[0]?.raw).toBe('${self:disk}');
|
|
142
|
+
expect(variables[1]?.raw).toBe('$self:vmid');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should handle braced variables with nested paths', () => {
|
|
146
|
+
const content = 'api = "${capability:auth.endpoint.url}/v1"';
|
|
147
|
+
const variables = parseVariables(content);
|
|
148
|
+
|
|
149
|
+
expect(variables).toHaveLength(1);
|
|
150
|
+
expect(variables[0]).toEqual({
|
|
151
|
+
type: 'capability',
|
|
152
|
+
path: 'auth.endpoint.url',
|
|
153
|
+
raw: '${capability:auth.endpoint.url}',
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should NOT match Terraform variables (no colon after type)', () => {
|
|
158
|
+
const content = `
|
|
159
|
+
hostname = "\${var.hostname}"
|
|
160
|
+
ip = "\${local.ip_address}"
|
|
161
|
+
id = "\${proxmox_lxc.caddy.id}"
|
|
162
|
+
cidr = "\${var.network.cidr}"
|
|
163
|
+
`;
|
|
164
|
+
const variables = parseVariables(content);
|
|
165
|
+
|
|
166
|
+
// Should find zero variables because Terraform uses dots, not colons
|
|
167
|
+
expect(variables).toHaveLength(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('should match our syntax but not Terraform syntax in same file', () => {
|
|
171
|
+
const content = `
|
|
172
|
+
# Our syntax (with colon)
|
|
173
|
+
size = "\${self:disk}G"
|
|
174
|
+
|
|
175
|
+
# Terraform syntax (with dots)
|
|
176
|
+
hostname = "\${var.hostname}"
|
|
177
|
+
|
|
178
|
+
# Our syntax again
|
|
179
|
+
vmid = $self:vmid
|
|
180
|
+
`;
|
|
181
|
+
const variables = parseVariables(content);
|
|
182
|
+
|
|
183
|
+
// Should only find our 2 variables
|
|
184
|
+
expect(variables).toHaveLength(2);
|
|
185
|
+
expect(variables[0]?.raw).toBe('${self:disk}');
|
|
186
|
+
expect(variables[1]?.raw).toBe('$self:vmid');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('hasVariables', () => {
|
|
191
|
+
test('should return true for content with variables', () => {
|
|
192
|
+
expect(hasVariables('ip: $self:container_ip')).toBe(true);
|
|
193
|
+
expect(hasVariables('$secret:key')).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should return true for content with braced variables', () => {
|
|
197
|
+
expect(hasVariables('size: "${self:disk}G"')).toBe(true);
|
|
198
|
+
expect(hasVariables('${system:url}')).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('should return false for content without variables', () => {
|
|
202
|
+
expect(hasVariables('no variables')).toBe(false);
|
|
203
|
+
expect(hasVariables('dollar sign $ but not a variable')).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('isValidVariableFormat', () => {
|
|
208
|
+
test('should validate correct variable formats', () => {
|
|
209
|
+
expect(isValidVariableFormat('$self:container_ip')).toBe(true);
|
|
210
|
+
expect(isValidVariableFormat('$system:management.ip')).toBe(true);
|
|
211
|
+
expect(isValidVariableFormat('$secret:api_key')).toBe(true);
|
|
212
|
+
expect(isValidVariableFormat('$capability:dns_external.nameserver')).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('should validate correct braced variable formats', () => {
|
|
216
|
+
expect(isValidVariableFormat('${self:disk}')).toBe(true);
|
|
217
|
+
expect(isValidVariableFormat('${system:url}')).toBe(true);
|
|
218
|
+
expect(isValidVariableFormat('${capability:auth.endpoint}')).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should reject invalid variable formats', () => {
|
|
222
|
+
expect(isValidVariableFormat('self:container_ip')).toBe(false); // missing $
|
|
223
|
+
expect(isValidVariableFormat('$unknown:value')).toBe(false); // invalid type
|
|
224
|
+
expect(isValidVariableFormat('$self:')).toBe(false); // missing path
|
|
225
|
+
expect(isValidVariableFormat('$self')).toBe(false); // missing colon and path
|
|
226
|
+
expect(isValidVariableFormat('$self:123')).toBe(false); // path starts with number
|
|
227
|
+
expect(isValidVariableFormat('$self:path with spaces')).toBe(false); // spaces not allowed
|
|
228
|
+
expect(isValidVariableFormat('${self:test')).toBe(false); // missing closing brace
|
|
229
|
+
expect(isValidVariableFormat('$self:test}')).toBe(false); // mismatched brace
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { VariableReference } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regular expression to match variable references
|
|
5
|
+
* Supports two syntaxes:
|
|
6
|
+
* 1. ${type:path.to.value} - Explicit braces for concatenation (e.g., "${self:disk}G")
|
|
7
|
+
* 2. $type:path.to.value - Simple syntax for standalone variables (e.g., "vmid = $self:vmid")
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - $self:container_ip
|
|
11
|
+
* - ${self:disk}G
|
|
12
|
+
* - $capability:dns_registrar.primary_domain
|
|
13
|
+
* - ${system:base_url}/api
|
|
14
|
+
*
|
|
15
|
+
* Path must start with letter or underscore, not digit
|
|
16
|
+
*/
|
|
17
|
+
const VARIABLE_PATTERN =
|
|
18
|
+
/\$\{(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)\}|\$(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse template content to extract variable references
|
|
22
|
+
*
|
|
23
|
+
* Policy function (Rule 10.1) - parses only, no side effects
|
|
24
|
+
*
|
|
25
|
+
* @param content - Template content
|
|
26
|
+
* @returns Array of variable references found in template
|
|
27
|
+
*/
|
|
28
|
+
export function parseVariables(content: string): VariableReference[] {
|
|
29
|
+
const variables: VariableReference[] = [];
|
|
30
|
+
const matches = content.matchAll(VARIABLE_PATTERN);
|
|
31
|
+
|
|
32
|
+
for (const match of matches) {
|
|
33
|
+
const [raw, bracedType, bracedPath, simpleType, simplePath] = match;
|
|
34
|
+
|
|
35
|
+
// Check which syntax matched (braced vs simple)
|
|
36
|
+
const type = bracedType || simpleType;
|
|
37
|
+
const path = bracedPath || simplePath;
|
|
38
|
+
|
|
39
|
+
if (type && path) {
|
|
40
|
+
variables.push({
|
|
41
|
+
type: type as VariableReference['type'],
|
|
42
|
+
path,
|
|
43
|
+
raw,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return variables;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a string contains any variable references
|
|
53
|
+
*
|
|
54
|
+
* @param content - String to check
|
|
55
|
+
* @returns True if content contains variables
|
|
56
|
+
*/
|
|
57
|
+
export function hasVariables(content: string): boolean {
|
|
58
|
+
// Create new regex without state to avoid issues with global flag
|
|
59
|
+
// Matches both ${type:path} and $type:path
|
|
60
|
+
const pattern = /\$\{?(self|system|system_secret|secret|capability):([a-zA-Z_][a-zA-Z0-9_.-]*)/;
|
|
61
|
+
return pattern.test(content);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate variable reference format
|
|
66
|
+
*
|
|
67
|
+
* @param variable - Variable string to validate
|
|
68
|
+
* @returns True if format is valid
|
|
69
|
+
*/
|
|
70
|
+
export function isValidVariableFormat(variable: string): boolean {
|
|
71
|
+
// Path must start with letter or underscore, not digit
|
|
72
|
+
// Accepts both ${type:path} and $type:path
|
|
73
|
+
const pattern =
|
|
74
|
+
/^(?:\$\{(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*\}|\$(self|system|system_secret|secret|capability):[a-zA-Z_][a-zA-Z0-9_.-]*)$/;
|
|
75
|
+
return pattern.test(variable);
|
|
76
|
+
}
|