@salte-common/terraflow 0.1.0-alpha.1
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/LICENSE +22 -0
- package/README.md +278 -0
- package/RELEASE_SUMMARY.md +53 -0
- package/STANDARDS_COMPLIANCE.md +85 -0
- package/bin/terraflow.js +3 -0
- package/bin/tf.js +3 -0
- package/dist/commands/apply.d.ts +7 -0
- package/dist/commands/apply.js +12 -0
- package/dist/commands/base.d.ts +7 -0
- package/dist/commands/base.js +12 -0
- package/dist/commands/config.d.ts +25 -0
- package/dist/commands/config.js +354 -0
- package/dist/commands/destroy.d.ts +7 -0
- package/dist/commands/destroy.js +12 -0
- package/dist/commands/init.d.ts +68 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/plan.d.ts +7 -0
- package/dist/commands/plan.js +12 -0
- package/dist/core/backend-state.d.ts +25 -0
- package/dist/core/backend-state.js +77 -0
- package/dist/core/config.d.ts +83 -0
- package/dist/core/config.js +295 -0
- package/dist/core/context.d.ts +52 -0
- package/dist/core/context.js +192 -0
- package/dist/core/environment.d.ts +62 -0
- package/dist/core/environment.js +205 -0
- package/dist/core/errors.d.ts +22 -0
- package/dist/core/errors.js +36 -0
- package/dist/core/plugin-loader.d.ts +21 -0
- package/dist/core/plugin-loader.js +136 -0
- package/dist/core/terraform.d.ts +45 -0
- package/dist/core/terraform.js +247 -0
- package/dist/core/validator.d.ts +103 -0
- package/dist/core/validator.js +304 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +184 -0
- package/dist/plugins/auth/aws-assume-role.d.ts +10 -0
- package/dist/plugins/auth/aws-assume-role.js +110 -0
- package/dist/plugins/auth/azure-service-principal.d.ts +10 -0
- package/dist/plugins/auth/azure-service-principal.js +99 -0
- package/dist/plugins/auth/gcp-service-account.d.ts +10 -0
- package/dist/plugins/auth/gcp-service-account.js +105 -0
- package/dist/plugins/backends/azurerm.d.ts +10 -0
- package/dist/plugins/backends/azurerm.js +117 -0
- package/dist/plugins/backends/gcs.d.ts +10 -0
- package/dist/plugins/backends/gcs.js +75 -0
- package/dist/plugins/backends/local.d.ts +11 -0
- package/dist/plugins/backends/local.js +37 -0
- package/dist/plugins/backends/s3.d.ts +10 -0
- package/dist/plugins/backends/s3.js +185 -0
- package/dist/plugins/secrets/aws-secrets.d.ts +12 -0
- package/dist/plugins/secrets/aws-secrets.js +125 -0
- package/dist/plugins/secrets/azure-keyvault.d.ts +12 -0
- package/dist/plugins/secrets/azure-keyvault.js +178 -0
- package/dist/plugins/secrets/env.d.ts +24 -0
- package/dist/plugins/secrets/env.js +62 -0
- package/dist/plugins/secrets/gcp-secret-manager.d.ts +12 -0
- package/dist/plugins/secrets/gcp-secret-manager.js +157 -0
- package/dist/templates/application/go/go.mod.template +4 -0
- package/dist/templates/application/go/main.template +8 -0
- package/dist/templates/application/go/test.template +11 -0
- package/dist/templates/application/javascript/main.template +14 -0
- package/dist/templates/application/javascript/test.template +8 -0
- package/dist/templates/application/python/main.template +13 -0
- package/dist/templates/application/python/requirements.txt.template +3 -0
- package/dist/templates/application/python/test.template +8 -0
- package/dist/templates/application/typescript/main.template +14 -0
- package/dist/templates/application/typescript/test.template +8 -0
- package/dist/templates/application/typescript/tsconfig.json.template +20 -0
- package/dist/templates/config/README.md.template +82 -0
- package/dist/templates/config/env.example.template +22 -0
- package/dist/templates/config/gitignore.template +40 -0
- package/dist/templates/config/tfwconfig.yml.template +69 -0
- package/dist/templates/templates/application/go/go.mod.template +4 -0
- package/dist/templates/templates/application/go/main.template +8 -0
- package/dist/templates/templates/application/go/test.template +11 -0
- package/dist/templates/templates/application/javascript/main.template +14 -0
- package/dist/templates/templates/application/javascript/test.template +8 -0
- package/dist/templates/templates/application/python/main.template +13 -0
- package/dist/templates/templates/application/python/requirements.txt.template +3 -0
- package/dist/templates/templates/application/python/test.template +8 -0
- package/dist/templates/templates/application/typescript/main.template +14 -0
- package/dist/templates/templates/application/typescript/test.template +8 -0
- package/dist/templates/templates/application/typescript/tsconfig.json.template +20 -0
- package/dist/templates/templates/config/README.md.template +82 -0
- package/dist/templates/templates/config/env.example.template +22 -0
- package/dist/templates/templates/config/gitignore.template +40 -0
- package/dist/templates/templates/config/tfwconfig.yml.template +69 -0
- package/dist/templates/templates/terraform/aws/_init.tf.template +24 -0
- package/dist/templates/templates/terraform/aws/inputs.tf.template +11 -0
- package/dist/templates/templates/terraform/azure/_init.tf.template +19 -0
- package/dist/templates/templates/terraform/azure/inputs.tf.template +11 -0
- package/dist/templates/templates/terraform/gcp/_init.tf.template +20 -0
- package/dist/templates/templates/terraform/gcp/inputs.tf.template +16 -0
- package/dist/templates/templates/terraform/locals.tf.template +9 -0
- package/dist/templates/templates/terraform/main.tf.template +8 -0
- package/dist/templates/templates/terraform/modules/inputs.tf.template +5 -0
- package/dist/templates/templates/terraform/modules/main.tf.template +2 -0
- package/dist/templates/templates/terraform/modules/outputs.tf.template +2 -0
- package/dist/templates/templates/terraform/outputs.tf.template +6 -0
- package/dist/templates/terraform/aws/_init.tf.template +24 -0
- package/dist/templates/terraform/aws/inputs.tf.template +11 -0
- package/dist/templates/terraform/azure/_init.tf.template +19 -0
- package/dist/templates/terraform/azure/inputs.tf.template +11 -0
- package/dist/templates/terraform/gcp/_init.tf.template +20 -0
- package/dist/templates/terraform/gcp/inputs.tf.template +16 -0
- package/dist/templates/terraform/locals.tf.template +9 -0
- package/dist/templates/terraform/main.tf.template +8 -0
- package/dist/templates/terraform/modules/inputs.tf.template +5 -0
- package/dist/templates/terraform/modules/main.tf.template +2 -0
- package/dist/templates/terraform/modules/outputs.tf.template +2 -0
- package/dist/templates/terraform/outputs.tf.template +6 -0
- package/dist/types/config.d.ts +92 -0
- package/dist/types/config.js +6 -0
- package/dist/types/context.d.ts +59 -0
- package/dist/types/context.js +6 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +23 -0
- package/dist/types/plugins.d.ts +77 -0
- package/dist/types/plugins.js +6 -0
- package/dist/utils/cloud.d.ts +43 -0
- package/dist/utils/cloud.js +150 -0
- package/dist/utils/git.d.ts +88 -0
- package/dist/utils/git.js +258 -0
- package/dist/utils/logger.d.ts +67 -0
- package/dist/utils/logger.js +121 -0
- package/dist/utils/scaffolding.d.ts +92 -0
- package/dist/utils/scaffolding.js +338 -0
- package/dist/utils/templates.d.ts +25 -0
- package/dist/utils/templates.js +70 -0
- package/package.json +60 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Terraform executor
|
|
4
|
+
* Executes terraform commands with proper environment setup
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.TerraformExecutor = void 0;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
const validator_1 = require("./validator");
|
|
11
|
+
const environment_1 = require("./environment");
|
|
12
|
+
const plugin_loader_1 = require("./plugin-loader");
|
|
13
|
+
const backend_state_1 = require("./backend-state");
|
|
14
|
+
const errors_1 = require("./errors");
|
|
15
|
+
/**
|
|
16
|
+
* Terraform executor for running terraform commands
|
|
17
|
+
*/
|
|
18
|
+
class TerraformExecutor {
|
|
19
|
+
/**
|
|
20
|
+
* Execute full terraform workflow
|
|
21
|
+
* @param command - Terraform command (e.g., 'plan', 'apply', 'destroy')
|
|
22
|
+
* @param args - Additional terraform arguments
|
|
23
|
+
* @param config - Terraflow configuration
|
|
24
|
+
* @param context - Execution context
|
|
25
|
+
* @param options - Execution options
|
|
26
|
+
*/
|
|
27
|
+
static async execute(command, args, config, context, options = {}) {
|
|
28
|
+
// 1. Run validations
|
|
29
|
+
logger_1.Logger.info('🔍 Running validations...');
|
|
30
|
+
const validationResult = await validator_1.Validator.validate(command, config, context, {
|
|
31
|
+
skipCommitCheck: options.skipCommitCheck || config['skip-commit-check'] || false,
|
|
32
|
+
dryRun: options.dryRun || false,
|
|
33
|
+
});
|
|
34
|
+
if (!validationResult.passed) {
|
|
35
|
+
logger_1.Logger.error('Validation failed:');
|
|
36
|
+
for (const error of validationResult.errors) {
|
|
37
|
+
logger_1.Logger.error(` - ${error}`);
|
|
38
|
+
}
|
|
39
|
+
if (validationResult.warnings.length > 0) {
|
|
40
|
+
logger_1.Logger.warn('Warnings:');
|
|
41
|
+
for (const warning of validationResult.warnings) {
|
|
42
|
+
logger_1.Logger.warn(` - ${warning}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new errors_1.ConfigError('Validation failed');
|
|
46
|
+
}
|
|
47
|
+
if (validationResult.warnings.length > 0) {
|
|
48
|
+
for (const warning of validationResult.warnings) {
|
|
49
|
+
logger_1.Logger.warn(warning);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
logger_1.Logger.info('✅ All validations passed');
|
|
53
|
+
// 2. Setup environment
|
|
54
|
+
logger_1.Logger.info('🔧 Setting up environment...');
|
|
55
|
+
const updatedContext = await environment_1.EnvironmentSetup.setup(config, context);
|
|
56
|
+
logger_1.Logger.info('✅ Environment setup complete');
|
|
57
|
+
// 3. Detect backend migration
|
|
58
|
+
if (config.backend) {
|
|
59
|
+
const previousBackendType = (0, backend_state_1.detectBackendMigration)(updatedContext.workingDir, config.backend);
|
|
60
|
+
if (previousBackendType && previousBackendType !== config.backend.type) {
|
|
61
|
+
logger_1.Logger.warn(`⚠️ Backend changed from '${previousBackendType}' to '${config.backend.type}'. Terraform will prompt to migrate state.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// 4. Execute auth plugin (if configured)
|
|
65
|
+
if (config.auth?.assume_role ||
|
|
66
|
+
config.auth?.service_principal ||
|
|
67
|
+
config.auth?.service_account) {
|
|
68
|
+
logger_1.Logger.info('🔐 Authenticating...');
|
|
69
|
+
try {
|
|
70
|
+
let authPlugin;
|
|
71
|
+
if (config.auth.assume_role) {
|
|
72
|
+
authPlugin = await (0, plugin_loader_1.loadAuthPlugin)('aws-assume-role');
|
|
73
|
+
}
|
|
74
|
+
else if (config.auth.service_principal) {
|
|
75
|
+
authPlugin = await (0, plugin_loader_1.loadAuthPlugin)('azure-service-principal');
|
|
76
|
+
}
|
|
77
|
+
else if (config.auth.service_account) {
|
|
78
|
+
authPlugin = await (0, plugin_loader_1.loadAuthPlugin)('gcp-service-account');
|
|
79
|
+
}
|
|
80
|
+
if (authPlugin) {
|
|
81
|
+
await authPlugin.validate(config.auth);
|
|
82
|
+
const credentials = await authPlugin.authenticate(config.auth, updatedContext);
|
|
83
|
+
// Set credentials as environment variables
|
|
84
|
+
for (const key in credentials) {
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(credentials, key)) {
|
|
86
|
+
process.env[key] = credentials[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
logger_1.Logger.info('✅ Authentication successful');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
logger_1.Logger.error(`Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 5. Execute secrets plugin (if configured)
|
|
98
|
+
if (config.secrets) {
|
|
99
|
+
logger_1.Logger.info(`🔑 Fetching secrets from ${config.secrets.provider}...`);
|
|
100
|
+
try {
|
|
101
|
+
const secretsPlugin = await (0, plugin_loader_1.loadSecretsPlugin)(config.secrets.provider);
|
|
102
|
+
await secretsPlugin.validate(config.secrets);
|
|
103
|
+
const secrets = await secretsPlugin.getSecrets(config.secrets, updatedContext);
|
|
104
|
+
// Set secrets as environment variables (already prefixed with TF_VAR_)
|
|
105
|
+
for (const key in secrets) {
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(secrets, key)) {
|
|
107
|
+
process.env[key] = secrets[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
logger_1.Logger.info(`✅ Loaded ${Object.keys(secrets).length} Terraform variables from secrets`);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
logger_1.Logger.error(`Failed to fetch secrets: ${error instanceof Error ? error.message : String(error)}`);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 6. Execute backend plugin
|
|
118
|
+
const backendType = config.backend?.type || 'local';
|
|
119
|
+
logger_1.Logger.info(`📦 Configuring ${backendType} backend...`);
|
|
120
|
+
try {
|
|
121
|
+
const backendPlugin = await (0, plugin_loader_1.loadBackendPlugin)(backendType);
|
|
122
|
+
await backendPlugin.validate(config.backend || { type: 'local' });
|
|
123
|
+
// Optional setup hook
|
|
124
|
+
if (backendPlugin.setup) {
|
|
125
|
+
await backendPlugin.setup(config.backend || { type: 'local' }, updatedContext);
|
|
126
|
+
}
|
|
127
|
+
const backendArgs = await backendPlugin.getBackendConfig(config.backend || { type: 'local' }, updatedContext);
|
|
128
|
+
// Save backend state for migration detection
|
|
129
|
+
if (config.backend) {
|
|
130
|
+
(0, backend_state_1.saveBackendState)(updatedContext.workingDir, config.backend);
|
|
131
|
+
}
|
|
132
|
+
if (options.dryRun) {
|
|
133
|
+
logger_1.Logger.info('🔍 DRY RUN MODE - Terraform commands will not be executed');
|
|
134
|
+
logger_1.Logger.info('═══════════════════════════════════════════════════════');
|
|
135
|
+
logger_1.Logger.info('Would execute:');
|
|
136
|
+
logger_1.Logger.info('═══════════════════════════════════════════════════════');
|
|
137
|
+
logger_1.Logger.info(`Workspace: ${updatedContext.workspace}`);
|
|
138
|
+
logger_1.Logger.info(`Working dir: ${updatedContext.workingDir}`);
|
|
139
|
+
logger_1.Logger.info(`Backend: ${backendType}`);
|
|
140
|
+
if (backendArgs.length > 0) {
|
|
141
|
+
logger_1.Logger.info('Backend init args:');
|
|
142
|
+
for (const arg of backendArgs) {
|
|
143
|
+
logger_1.Logger.info(` ${arg}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
logger_1.Logger.info(`Terraform command: terraform ${command} ${args.join(' ')}`);
|
|
147
|
+
logger_1.Logger.info('═══════════════════════════════════════════════════════');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// 7. Run terraform init with backend config
|
|
151
|
+
await TerraformExecutor.init(backendType, backendArgs, updatedContext.workingDir);
|
|
152
|
+
// 8. Select/create workspace
|
|
153
|
+
await TerraformExecutor.workspace(updatedContext.workspace, updatedContext.workingDir);
|
|
154
|
+
// 9. Execute terraform command
|
|
155
|
+
await TerraformExecutor.runCommand(command, args, updatedContext.workingDir);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger_1.Logger.error(`Backend setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Initialize terraform with backend configuration
|
|
165
|
+
* For local backend, runs terraform init without backend-config flags
|
|
166
|
+
* @param backendType - Backend type (e.g., 'local', 's3', 'azurerm', 'gcs')
|
|
167
|
+
* @param backendArgs - Backend configuration arguments (-backend-config flags)
|
|
168
|
+
* @param workingDir - Terraform working directory
|
|
169
|
+
*/
|
|
170
|
+
static async init(backendType, backendArgs, workingDir) {
|
|
171
|
+
const args = ['init'];
|
|
172
|
+
// For local backend, skip backend-config arguments
|
|
173
|
+
// Terraform uses local backend by default if no backend is configured
|
|
174
|
+
if (backendType !== 'local' && backendArgs.length > 0) {
|
|
175
|
+
args.push(...backendArgs);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
logger_1.Logger.debug(`Executing: terraform ${args.join(' ')} in ${workingDir}`);
|
|
179
|
+
(0, child_process_1.execSync)(`terraform ${args.join(' ')}`, {
|
|
180
|
+
cwd: workingDir,
|
|
181
|
+
stdio: 'inherit',
|
|
182
|
+
encoding: 'utf8',
|
|
183
|
+
});
|
|
184
|
+
logger_1.Logger.info('✅ Terraform initialized successfully');
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
logger_1.Logger.error(`Failed to initialize Terraform: ${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Select or create workspace
|
|
193
|
+
* @param workspaceName - Workspace name
|
|
194
|
+
* @param workingDir - Terraform working directory
|
|
195
|
+
*/
|
|
196
|
+
static async workspace(workspaceName, workingDir) {
|
|
197
|
+
try {
|
|
198
|
+
// Try to select existing workspace
|
|
199
|
+
logger_1.Logger.debug(`Selecting workspace: ${workspaceName}`);
|
|
200
|
+
(0, child_process_1.execSync)(`terraform workspace select ${workspaceName}`, {
|
|
201
|
+
cwd: workingDir,
|
|
202
|
+
stdio: 'pipe',
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
});
|
|
205
|
+
logger_1.Logger.debug(`Workspace ${workspaceName} selected`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Workspace doesn't exist, create it
|
|
209
|
+
try {
|
|
210
|
+
logger_1.Logger.debug(`Creating workspace: ${workspaceName}`);
|
|
211
|
+
(0, child_process_1.execSync)(`terraform workspace new ${workspaceName}`, {
|
|
212
|
+
cwd: workingDir,
|
|
213
|
+
stdio: 'inherit',
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
});
|
|
216
|
+
logger_1.Logger.info(`✅ Workspace ${workspaceName} created and selected`);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger_1.Logger.error(`Failed to create workspace ${workspaceName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Execute terraform command
|
|
226
|
+
* @param command - Terraform command (e.g., 'plan', 'apply', 'destroy')
|
|
227
|
+
* @param args - Additional terraform arguments
|
|
228
|
+
* @param workingDir - Terraform working directory
|
|
229
|
+
*/
|
|
230
|
+
static async runCommand(command, args, workingDir) {
|
|
231
|
+
const terraformArgs = [command, ...args];
|
|
232
|
+
try {
|
|
233
|
+
logger_1.Logger.info(`🚀 Executing: terraform ${terraformArgs.join(' ')}`);
|
|
234
|
+
(0, child_process_1.execSync)(`terraform ${terraformArgs.join(' ')}`, {
|
|
235
|
+
cwd: workingDir,
|
|
236
|
+
stdio: 'inherit',
|
|
237
|
+
encoding: 'utf8',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
logger_1.Logger.error(`Terraform command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
exports.TerraformExecutor = TerraformExecutor;
|
|
247
|
+
//# sourceMappingURL=terraform.js.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation engine
|
|
3
|
+
* Validates configuration, workspace, and environment
|
|
4
|
+
*/
|
|
5
|
+
import type { TerraflowConfig, ValidationConfig } from '../types/config';
|
|
6
|
+
import type { ExecutionContext } from '../types/context';
|
|
7
|
+
/**
|
|
8
|
+
* Command categories for validation
|
|
9
|
+
*/
|
|
10
|
+
export declare const FULL_VALIDATION_COMMANDS: string[];
|
|
11
|
+
export declare const BACKEND_REQUIRED_COMMANDS: string[];
|
|
12
|
+
export declare const MINIMAL_VALIDATION_COMMANDS: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Validation result
|
|
15
|
+
*/
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
passed: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validation engine for Terraflow
|
|
23
|
+
*/
|
|
24
|
+
export declare class Validator {
|
|
25
|
+
/**
|
|
26
|
+
* Validate terraform is installed and accessible
|
|
27
|
+
* @throws {ValidationError} If terraform is not installed
|
|
28
|
+
*/
|
|
29
|
+
static validateTerraformInstalled(): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Check if git is available (not fatal if missing)
|
|
32
|
+
* @returns True if git is available
|
|
33
|
+
*/
|
|
34
|
+
static validateGitRepo(cwd?: string): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Validate git working directory is clean (no uncommitted changes)
|
|
37
|
+
* @param cwd - Current working directory
|
|
38
|
+
* @throws {ValidationError} If working directory is not clean
|
|
39
|
+
*/
|
|
40
|
+
static validateGitCommit(cwd?: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Validate workspace name matches terraform naming rules
|
|
43
|
+
* Must match: /^[a-zA-Z0-9_-]+$/
|
|
44
|
+
* @param workspace - Workspace name to validate
|
|
45
|
+
* @throws {ValidationError} If workspace name is invalid
|
|
46
|
+
*/
|
|
47
|
+
static validateWorkspaceName(workspace: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Validate workspace is in allowed list (if configured)
|
|
50
|
+
* @param workspace - Workspace name
|
|
51
|
+
* @param config - Validation configuration
|
|
52
|
+
* @throws {ValidationError} If workspace is not allowed
|
|
53
|
+
*/
|
|
54
|
+
static validateAllowedWorkspace(workspace: string, config?: ValidationConfig): void;
|
|
55
|
+
/**
|
|
56
|
+
* Validate required variables are set
|
|
57
|
+
* Checks for TF_VAR_* environment variables
|
|
58
|
+
* @param requiredVars - List of required variable names (without TF_VAR_ prefix)
|
|
59
|
+
* @param env - Environment variables
|
|
60
|
+
* @throws {ValidationError} If required variables are missing
|
|
61
|
+
*/
|
|
62
|
+
static validateRequiredVariables(requiredVars: string[], env?: Record<string, string>): void;
|
|
63
|
+
/**
|
|
64
|
+
* Validate backend configuration
|
|
65
|
+
* @param config - Terraflow configuration
|
|
66
|
+
* @throws {ValidationError} If backend configuration is invalid
|
|
67
|
+
*/
|
|
68
|
+
static validateBackendConfig(config: TerraflowConfig): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Validate cloud credentials are available (placeholder for future implementation)
|
|
71
|
+
* @param backendType - Backend type
|
|
72
|
+
* @param cloud - Cloud information
|
|
73
|
+
* @throws {ValidationError} If credentials are missing
|
|
74
|
+
*/
|
|
75
|
+
static validateCloudCredentials(backendType: string, cloud: ExecutionContext['cloud']): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Validate plugin configurations (placeholder for future implementation)
|
|
78
|
+
* @param _config - Terraflow configuration
|
|
79
|
+
* @throws {ValidationError} If plugin configuration is invalid
|
|
80
|
+
*/
|
|
81
|
+
static validatePluginConfigs(_config: TerraflowConfig): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Run validations based on command type
|
|
84
|
+
* @param command - Terraform command
|
|
85
|
+
* @param config - Terraflow configuration
|
|
86
|
+
* @param context - Execution context
|
|
87
|
+
* @param options - Validation options
|
|
88
|
+
* @returns Validation result
|
|
89
|
+
*/
|
|
90
|
+
static validate(command: string, config: TerraflowConfig, context: ExecutionContext, options?: {
|
|
91
|
+
skipCommitCheck?: boolean;
|
|
92
|
+
dryRun?: boolean;
|
|
93
|
+
}): Promise<ValidationResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Run full validations for apply, destroy, import, refresh commands
|
|
96
|
+
*/
|
|
97
|
+
private static runFullValidations;
|
|
98
|
+
/**
|
|
99
|
+
* Run backend validations for plan, state, workspace, output, show commands
|
|
100
|
+
*/
|
|
101
|
+
private static runBackendValidations;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validation engine
|
|
4
|
+
* Validates configuration, workspace, and environment
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.Validator = exports.MINIMAL_VALIDATION_COMMANDS = exports.BACKEND_REQUIRED_COMMANDS = exports.FULL_VALIDATION_COMMANDS = void 0;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const git_1 = require("../utils/git");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
/**
|
|
13
|
+
* Command categories for validation
|
|
14
|
+
*/
|
|
15
|
+
exports.FULL_VALIDATION_COMMANDS = ['apply', 'destroy', 'import', 'refresh'];
|
|
16
|
+
exports.BACKEND_REQUIRED_COMMANDS = ['plan', 'state', 'workspace', 'output', 'show'];
|
|
17
|
+
exports.MINIMAL_VALIDATION_COMMANDS = ['fmt', 'validate', 'version', 'providers'];
|
|
18
|
+
/**
|
|
19
|
+
* Validation engine for Terraflow
|
|
20
|
+
*/
|
|
21
|
+
class Validator {
|
|
22
|
+
/**
|
|
23
|
+
* Validate terraform is installed and accessible
|
|
24
|
+
* @throws {ValidationError} If terraform is not installed
|
|
25
|
+
*/
|
|
26
|
+
static async validateTerraformInstalled() {
|
|
27
|
+
try {
|
|
28
|
+
(0, child_process_1.execSync)('terraform version', {
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new errors_1.ValidationError('Terraform is not installed or not available in PATH. Please install Terraform and ensure it is in your PATH.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if git is available (not fatal if missing)
|
|
39
|
+
* @returns True if git is available
|
|
40
|
+
*/
|
|
41
|
+
static async validateGitRepo(cwd = process.cwd()) {
|
|
42
|
+
return git_1.GitUtils.isGitRepository(cwd);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate git working directory is clean (no uncommitted changes)
|
|
46
|
+
* @param cwd - Current working directory
|
|
47
|
+
* @throws {ValidationError} If working directory is not clean
|
|
48
|
+
*/
|
|
49
|
+
static async validateGitCommit(cwd = process.cwd()) {
|
|
50
|
+
const isClean = await git_1.GitUtils.isClean(cwd);
|
|
51
|
+
if (!isClean) {
|
|
52
|
+
throw new errors_1.ValidationError('Git working directory has uncommitted changes. Please commit or stash your changes before running this command. Use --skip-commit-check to bypass this validation.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate workspace name matches terraform naming rules
|
|
57
|
+
* Must match: /^[a-zA-Z0-9_-]+$/
|
|
58
|
+
* @param workspace - Workspace name to validate
|
|
59
|
+
* @throws {ValidationError} If workspace name is invalid
|
|
60
|
+
*/
|
|
61
|
+
static validateWorkspaceName(workspace) {
|
|
62
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(workspace)) {
|
|
63
|
+
throw new errors_1.ValidationError(`Invalid workspace name "${workspace}". Workspace names must match /^[a-zA-Z0-9_-]+$/ (alphanumeric, underscore, hyphen only).`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validate workspace is in allowed list (if configured)
|
|
68
|
+
* @param workspace - Workspace name
|
|
69
|
+
* @param config - Validation configuration
|
|
70
|
+
* @throws {ValidationError} If workspace is not allowed
|
|
71
|
+
*/
|
|
72
|
+
static validateAllowedWorkspace(workspace, config) {
|
|
73
|
+
if (!config || !config.allowed_workspaces || config.allowed_workspaces.length === 0) {
|
|
74
|
+
// No restrictions
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!config.allowed_workspaces.includes(workspace)) {
|
|
78
|
+
throw new errors_1.ValidationError(`Workspace "${workspace}" is not in the allowed list. Allowed workspaces: ${config.allowed_workspaces.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate required variables are set
|
|
83
|
+
* Checks for TF_VAR_* environment variables
|
|
84
|
+
* @param requiredVars - List of required variable names (without TF_VAR_ prefix)
|
|
85
|
+
* @param env - Environment variables
|
|
86
|
+
* @throws {ValidationError} If required variables are missing
|
|
87
|
+
*/
|
|
88
|
+
static validateRequiredVariables(requiredVars, env = process.env) {
|
|
89
|
+
if (!requiredVars || requiredVars.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const missing = [];
|
|
93
|
+
for (const varName of requiredVars) {
|
|
94
|
+
const envVarName = `TF_VAR_${varName}`;
|
|
95
|
+
if (!env[envVarName]) {
|
|
96
|
+
missing.push(varName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (missing.length > 0) {
|
|
100
|
+
throw new errors_1.ValidationError(`Required Terraform variables are missing: ${missing.join(', ')}. Set them as environment variables: ${missing.map((v) => `TF_VAR_${v}`).join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Validate backend configuration
|
|
105
|
+
* @param config - Terraflow configuration
|
|
106
|
+
* @throws {ValidationError} If backend configuration is invalid
|
|
107
|
+
*/
|
|
108
|
+
static async validateBackendConfig(config) {
|
|
109
|
+
if (!config.backend) {
|
|
110
|
+
return; // Local backend, no validation needed
|
|
111
|
+
}
|
|
112
|
+
// For now, basic validation - plugins will do detailed validation
|
|
113
|
+
if (!config.backend.type) {
|
|
114
|
+
throw new errors_1.ValidationError('Backend type is required');
|
|
115
|
+
}
|
|
116
|
+
// TODO: Plugin-specific validation will be handled by backend plugins
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Validate cloud credentials are available (placeholder for future implementation)
|
|
120
|
+
* @param backendType - Backend type
|
|
121
|
+
* @param cloud - Cloud information
|
|
122
|
+
* @throws {ValidationError} If credentials are missing
|
|
123
|
+
*/
|
|
124
|
+
static async validateCloudCredentials(backendType, cloud) {
|
|
125
|
+
if (backendType === 'local') {
|
|
126
|
+
return; // No credentials needed for local backend
|
|
127
|
+
}
|
|
128
|
+
// TODO: Implement credential validation for AWS, Azure, GCP
|
|
129
|
+
// For now, just check if provider is detected
|
|
130
|
+
if (backendType === 's3' && cloud.provider !== 'aws') {
|
|
131
|
+
logger_1.Logger.warn('S3 backend configured but AWS provider not detected');
|
|
132
|
+
}
|
|
133
|
+
if (backendType === 'azurerm' && cloud.provider !== 'azure') {
|
|
134
|
+
logger_1.Logger.warn('Azure backend configured but Azure provider not detected');
|
|
135
|
+
}
|
|
136
|
+
if (backendType === 'gcs' && cloud.provider !== 'gcp') {
|
|
137
|
+
logger_1.Logger.warn('GCS backend configured but GCP provider not detected');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate plugin configurations (placeholder for future implementation)
|
|
142
|
+
* @param _config - Terraflow configuration
|
|
143
|
+
* @throws {ValidationError} If plugin configuration is invalid
|
|
144
|
+
*/
|
|
145
|
+
static async validatePluginConfigs(_config) {
|
|
146
|
+
// TODO: Plugin validation will be handled by plugin system
|
|
147
|
+
// This is a placeholder for future implementation
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Run validations based on command type
|
|
151
|
+
* @param command - Terraform command
|
|
152
|
+
* @param config - Terraflow configuration
|
|
153
|
+
* @param context - Execution context
|
|
154
|
+
* @param options - Validation options
|
|
155
|
+
* @returns Validation result
|
|
156
|
+
*/
|
|
157
|
+
static async validate(command, config, context, options = {}) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const warnings = [];
|
|
160
|
+
try {
|
|
161
|
+
// Always validate terraform installation
|
|
162
|
+
await Validator.validateTerraformInstalled();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
166
|
+
errors.push(message);
|
|
167
|
+
if (!options.dryRun) {
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
// Always validate workspace name format
|
|
173
|
+
Validator.validateWorkspaceName(context.workspace);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
177
|
+
errors.push(message);
|
|
178
|
+
if (!options.dryRun) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Check git availability (not fatal)
|
|
183
|
+
const gitAvailable = await Validator.validateGitRepo(context.workingDir);
|
|
184
|
+
if (!gitAvailable) {
|
|
185
|
+
warnings.push('Git repository not detected. Some features may not work correctly.');
|
|
186
|
+
}
|
|
187
|
+
// Command-specific validations
|
|
188
|
+
if (exports.FULL_VALIDATION_COMMANDS.includes(command)) {
|
|
189
|
+
await Validator.runFullValidations(config, context, options, errors, warnings);
|
|
190
|
+
}
|
|
191
|
+
else if (exports.BACKEND_REQUIRED_COMMANDS.includes(command)) {
|
|
192
|
+
await Validator.runBackendValidations(config, context, errors, warnings);
|
|
193
|
+
}
|
|
194
|
+
// MINIMAL_VALIDATION_COMMANDS only need terraform installed (already validated)
|
|
195
|
+
// In dry-run mode, return errors without throwing
|
|
196
|
+
if (options.dryRun && errors.length > 0) {
|
|
197
|
+
return {
|
|
198
|
+
passed: false,
|
|
199
|
+
errors,
|
|
200
|
+
warnings,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// In normal mode, throw if there are errors
|
|
204
|
+
if (errors.length > 0) {
|
|
205
|
+
throw new errors_1.ValidationError(`Validation failed:\n${errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
passed: true,
|
|
209
|
+
errors: [],
|
|
210
|
+
warnings,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Run full validations for apply, destroy, import, refresh commands
|
|
215
|
+
*/
|
|
216
|
+
static async runFullValidations(config, context, options, errors, _warnings) {
|
|
217
|
+
// Git working directory clean
|
|
218
|
+
if (!options.skipCommitCheck && !config['skip-commit-check']) {
|
|
219
|
+
try {
|
|
220
|
+
await Validator.validateGitCommit(context.workingDir);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
224
|
+
errors.push(message);
|
|
225
|
+
if (!options.dryRun) {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Workspace in allowed list
|
|
231
|
+
try {
|
|
232
|
+
Validator.validateAllowedWorkspace(context.workspace, config.validations);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
236
|
+
errors.push(message);
|
|
237
|
+
if (!options.dryRun) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Backend config valid
|
|
242
|
+
try {
|
|
243
|
+
await Validator.validateBackendConfig(config);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
247
|
+
errors.push(message);
|
|
248
|
+
if (!options.dryRun) {
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Cloud credentials available
|
|
253
|
+
if (config.backend && config.backend.type !== 'local') {
|
|
254
|
+
try {
|
|
255
|
+
await Validator.validateCloudCredentials(config.backend.type, context.cloud);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
259
|
+
errors.push(message);
|
|
260
|
+
if (!options.dryRun) {
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Plugin configs valid
|
|
266
|
+
try {
|
|
267
|
+
await Validator.validatePluginConfigs(config);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
271
|
+
errors.push(message);
|
|
272
|
+
if (!options.dryRun) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Run backend validations for plan, state, workspace, output, show commands
|
|
279
|
+
*/
|
|
280
|
+
static async runBackendValidations(config, context, errors, _warnings) {
|
|
281
|
+
// Backend config valid (if not local)
|
|
282
|
+
if (config.backend && config.backend.type !== 'local') {
|
|
283
|
+
try {
|
|
284
|
+
await Validator.validateBackendConfig(config);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
288
|
+
errors.push(message);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
// Cloud credentials available
|
|
292
|
+
try {
|
|
293
|
+
await Validator.validateCloudCredentials(config.backend.type, context.cloud);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
const message = error instanceof errors_1.ValidationError ? error.message : String(error);
|
|
297
|
+
errors.push(message);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
exports.Validator = Validator;
|
|
304
|
+
//# sourceMappingURL=validator.js.map
|