@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.
Files changed (131) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +278 -0
  3. package/RELEASE_SUMMARY.md +53 -0
  4. package/STANDARDS_COMPLIANCE.md +85 -0
  5. package/bin/terraflow.js +3 -0
  6. package/bin/tf.js +3 -0
  7. package/dist/commands/apply.d.ts +7 -0
  8. package/dist/commands/apply.js +12 -0
  9. package/dist/commands/base.d.ts +7 -0
  10. package/dist/commands/base.js +12 -0
  11. package/dist/commands/config.d.ts +25 -0
  12. package/dist/commands/config.js +354 -0
  13. package/dist/commands/destroy.d.ts +7 -0
  14. package/dist/commands/destroy.js +12 -0
  15. package/dist/commands/init.d.ts +68 -0
  16. package/dist/commands/init.js +131 -0
  17. package/dist/commands/plan.d.ts +7 -0
  18. package/dist/commands/plan.js +12 -0
  19. package/dist/core/backend-state.d.ts +25 -0
  20. package/dist/core/backend-state.js +77 -0
  21. package/dist/core/config.d.ts +83 -0
  22. package/dist/core/config.js +295 -0
  23. package/dist/core/context.d.ts +52 -0
  24. package/dist/core/context.js +192 -0
  25. package/dist/core/environment.d.ts +62 -0
  26. package/dist/core/environment.js +205 -0
  27. package/dist/core/errors.d.ts +22 -0
  28. package/dist/core/errors.js +36 -0
  29. package/dist/core/plugin-loader.d.ts +21 -0
  30. package/dist/core/plugin-loader.js +136 -0
  31. package/dist/core/terraform.d.ts +45 -0
  32. package/dist/core/terraform.js +247 -0
  33. package/dist/core/validator.d.ts +103 -0
  34. package/dist/core/validator.js +304 -0
  35. package/dist/index.d.ts +7 -0
  36. package/dist/index.js +184 -0
  37. package/dist/plugins/auth/aws-assume-role.d.ts +10 -0
  38. package/dist/plugins/auth/aws-assume-role.js +110 -0
  39. package/dist/plugins/auth/azure-service-principal.d.ts +10 -0
  40. package/dist/plugins/auth/azure-service-principal.js +99 -0
  41. package/dist/plugins/auth/gcp-service-account.d.ts +10 -0
  42. package/dist/plugins/auth/gcp-service-account.js +105 -0
  43. package/dist/plugins/backends/azurerm.d.ts +10 -0
  44. package/dist/plugins/backends/azurerm.js +117 -0
  45. package/dist/plugins/backends/gcs.d.ts +10 -0
  46. package/dist/plugins/backends/gcs.js +75 -0
  47. package/dist/plugins/backends/local.d.ts +11 -0
  48. package/dist/plugins/backends/local.js +37 -0
  49. package/dist/plugins/backends/s3.d.ts +10 -0
  50. package/dist/plugins/backends/s3.js +185 -0
  51. package/dist/plugins/secrets/aws-secrets.d.ts +12 -0
  52. package/dist/plugins/secrets/aws-secrets.js +125 -0
  53. package/dist/plugins/secrets/azure-keyvault.d.ts +12 -0
  54. package/dist/plugins/secrets/azure-keyvault.js +178 -0
  55. package/dist/plugins/secrets/env.d.ts +24 -0
  56. package/dist/plugins/secrets/env.js +62 -0
  57. package/dist/plugins/secrets/gcp-secret-manager.d.ts +12 -0
  58. package/dist/plugins/secrets/gcp-secret-manager.js +157 -0
  59. package/dist/templates/application/go/go.mod.template +4 -0
  60. package/dist/templates/application/go/main.template +8 -0
  61. package/dist/templates/application/go/test.template +11 -0
  62. package/dist/templates/application/javascript/main.template +14 -0
  63. package/dist/templates/application/javascript/test.template +8 -0
  64. package/dist/templates/application/python/main.template +13 -0
  65. package/dist/templates/application/python/requirements.txt.template +3 -0
  66. package/dist/templates/application/python/test.template +8 -0
  67. package/dist/templates/application/typescript/main.template +14 -0
  68. package/dist/templates/application/typescript/test.template +8 -0
  69. package/dist/templates/application/typescript/tsconfig.json.template +20 -0
  70. package/dist/templates/config/README.md.template +82 -0
  71. package/dist/templates/config/env.example.template +22 -0
  72. package/dist/templates/config/gitignore.template +40 -0
  73. package/dist/templates/config/tfwconfig.yml.template +69 -0
  74. package/dist/templates/templates/application/go/go.mod.template +4 -0
  75. package/dist/templates/templates/application/go/main.template +8 -0
  76. package/dist/templates/templates/application/go/test.template +11 -0
  77. package/dist/templates/templates/application/javascript/main.template +14 -0
  78. package/dist/templates/templates/application/javascript/test.template +8 -0
  79. package/dist/templates/templates/application/python/main.template +13 -0
  80. package/dist/templates/templates/application/python/requirements.txt.template +3 -0
  81. package/dist/templates/templates/application/python/test.template +8 -0
  82. package/dist/templates/templates/application/typescript/main.template +14 -0
  83. package/dist/templates/templates/application/typescript/test.template +8 -0
  84. package/dist/templates/templates/application/typescript/tsconfig.json.template +20 -0
  85. package/dist/templates/templates/config/README.md.template +82 -0
  86. package/dist/templates/templates/config/env.example.template +22 -0
  87. package/dist/templates/templates/config/gitignore.template +40 -0
  88. package/dist/templates/templates/config/tfwconfig.yml.template +69 -0
  89. package/dist/templates/templates/terraform/aws/_init.tf.template +24 -0
  90. package/dist/templates/templates/terraform/aws/inputs.tf.template +11 -0
  91. package/dist/templates/templates/terraform/azure/_init.tf.template +19 -0
  92. package/dist/templates/templates/terraform/azure/inputs.tf.template +11 -0
  93. package/dist/templates/templates/terraform/gcp/_init.tf.template +20 -0
  94. package/dist/templates/templates/terraform/gcp/inputs.tf.template +16 -0
  95. package/dist/templates/templates/terraform/locals.tf.template +9 -0
  96. package/dist/templates/templates/terraform/main.tf.template +8 -0
  97. package/dist/templates/templates/terraform/modules/inputs.tf.template +5 -0
  98. package/dist/templates/templates/terraform/modules/main.tf.template +2 -0
  99. package/dist/templates/templates/terraform/modules/outputs.tf.template +2 -0
  100. package/dist/templates/templates/terraform/outputs.tf.template +6 -0
  101. package/dist/templates/terraform/aws/_init.tf.template +24 -0
  102. package/dist/templates/terraform/aws/inputs.tf.template +11 -0
  103. package/dist/templates/terraform/azure/_init.tf.template +19 -0
  104. package/dist/templates/terraform/azure/inputs.tf.template +11 -0
  105. package/dist/templates/terraform/gcp/_init.tf.template +20 -0
  106. package/dist/templates/terraform/gcp/inputs.tf.template +16 -0
  107. package/dist/templates/terraform/locals.tf.template +9 -0
  108. package/dist/templates/terraform/main.tf.template +8 -0
  109. package/dist/templates/terraform/modules/inputs.tf.template +5 -0
  110. package/dist/templates/terraform/modules/main.tf.template +2 -0
  111. package/dist/templates/terraform/modules/outputs.tf.template +2 -0
  112. package/dist/templates/terraform/outputs.tf.template +6 -0
  113. package/dist/types/config.d.ts +92 -0
  114. package/dist/types/config.js +6 -0
  115. package/dist/types/context.d.ts +59 -0
  116. package/dist/types/context.js +6 -0
  117. package/dist/types/index.d.ts +7 -0
  118. package/dist/types/index.js +23 -0
  119. package/dist/types/plugins.d.ts +77 -0
  120. package/dist/types/plugins.js +6 -0
  121. package/dist/utils/cloud.d.ts +43 -0
  122. package/dist/utils/cloud.js +150 -0
  123. package/dist/utils/git.d.ts +88 -0
  124. package/dist/utils/git.js +258 -0
  125. package/dist/utils/logger.d.ts +67 -0
  126. package/dist/utils/logger.js +121 -0
  127. package/dist/utils/scaffolding.d.ts +92 -0
  128. package/dist/utils/scaffolding.js +338 -0
  129. package/dist/utils/templates.d.ts +25 -0
  130. package/dist/utils/templates.js +70 -0
  131. 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Terraflow CLI - Main entry point
4
+ * An opinionated Terraform workflow CLI with multi-cloud support
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=index.d.ts.map