env-secrets 0.1.10 → 0.3.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/.devcontainer/devcontainer.json +33 -0
- package/.dockerignore +9 -0
- package/.eslintignore +4 -2
- package/.github/dependabot.yml +4 -0
- package/.github/workflows/build-main.yml +6 -2
- package/.github/workflows/deploy-docs.yml +50 -0
- package/.github/workflows/e2e-tests.yaml +54 -0
- package/.github/workflows/lint.yaml +6 -2
- package/.github/workflows/release.yml +13 -3
- package/.github/workflows/snyk.yaml +5 -1
- package/.github/workflows/unittests.yaml +18 -6
- package/.lintstagedrc +2 -7
- package/.prettierignore +6 -0
- package/AGENTS.md +149 -0
- package/Dockerfile +14 -0
- package/README.md +507 -36
- package/__e2e__/README.md +160 -0
- package/__e2e__/index.test.ts +339 -0
- package/__e2e__/setup.ts +58 -0
- package/__e2e__/utils/debug-logger.ts +45 -0
- package/__e2e__/utils/test-utils.ts +645 -0
- package/__tests__/index.test.ts +573 -31
- package/__tests__/vaults/secretsmanager.test.ts +460 -0
- package/__tests__/vaults/utils.test.ts +183 -0
- package/__tests__/version.test.ts +8 -0
- package/dist/index.js +36 -10
- package/dist/vaults/secretsmanager.js +44 -43
- package/dist/vaults/utils.js +2 -2
- package/docker-compose.yaml +29 -0
- package/docs/AWS.md +257 -0
- package/jest.config.js +4 -1
- package/jest.e2e.config.js +8 -0
- package/package.json +18 -10
- package/src/index.ts +44 -10
- package/src/vaults/secretsmanager.ts +48 -48
- package/src/vaults/utils.ts +6 -4
- package/website/docs/advanced-usage.mdx +399 -0
- package/website/docs/best-practices.mdx +416 -0
- package/website/docs/cli-reference.mdx +204 -0
- package/website/docs/examples.mdx +960 -0
- package/website/docs/faq.mdx +302 -0
- package/website/docs/index.mdx +56 -0
- package/website/docs/installation.mdx +30 -0
- package/website/docs/overview.mdx +17 -0
- package/website/docs/production-deployment.mdx +622 -0
- package/website/docs/providers/aws-secrets-manager.mdx +28 -0
- package/website/docs/security.mdx +122 -0
- package/website/docs/troubleshooting.mdx +236 -0
- package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
- package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
- package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
- package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
- package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
- package/website/docusaurus.config.ts +89 -0
- package/website/package.json +21 -0
- package/website/sidebars.ts +33 -0
- package/website/src/css/custom.css +1 -0
- package/website/static/img/env-secrets.png +0 -0
- package/website/static/img/favicon.ico +0 -0
- package/website/static/img/logo.svg +4 -0
- package/website/yarn.lock +8764 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import { debugLog, debugError, debugWarn } from './debug-logger';
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// Generate a unique character string for this test run
|
|
12
|
+
export function generateUniqueRunId(): string {
|
|
13
|
+
return crypto.randomBytes(8).toString('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Enhanced error class for awslocal command failures
|
|
17
|
+
export class AwslocalCommandError extends Error {
|
|
18
|
+
public readonly command: string;
|
|
19
|
+
public readonly exitCode: number;
|
|
20
|
+
public readonly stdout: string;
|
|
21
|
+
public readonly stderr: string;
|
|
22
|
+
public readonly environment: Record<string, string>;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
command: string,
|
|
26
|
+
exitCode: number,
|
|
27
|
+
stdout: string,
|
|
28
|
+
stderr: string,
|
|
29
|
+
environment: Record<string, string>,
|
|
30
|
+
originalError?: Error
|
|
31
|
+
) {
|
|
32
|
+
const message =
|
|
33
|
+
`awslocal command failed with exit code ${exitCode}\n` +
|
|
34
|
+
`Command: ${command}\n` +
|
|
35
|
+
`Exit Code: ${exitCode}\n` +
|
|
36
|
+
`Stdout: ${stdout || '(empty)'}\n` +
|
|
37
|
+
`Stderr: ${stderr || '(empty)'}\n` +
|
|
38
|
+
`Environment: ${JSON.stringify(environment, null, 2)}`;
|
|
39
|
+
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'AwslocalCommandError';
|
|
42
|
+
this.command = command;
|
|
43
|
+
this.exitCode = exitCode;
|
|
44
|
+
this.stdout = stdout;
|
|
45
|
+
this.stderr = stderr;
|
|
46
|
+
this.environment = environment;
|
|
47
|
+
|
|
48
|
+
// Preserve original error stack if available
|
|
49
|
+
if (originalError && originalError.stack) {
|
|
50
|
+
this.stack = originalError.stack;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Enhanced execAsync function with better error handling
|
|
56
|
+
export async function execAwslocalCommand(
|
|
57
|
+
command: string,
|
|
58
|
+
environment: Record<string, string> = {},
|
|
59
|
+
options: { timeout?: number; cwd?: string } = {}
|
|
60
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
61
|
+
const env = { ...process.env, ...environment };
|
|
62
|
+
const execOptions = {
|
|
63
|
+
env,
|
|
64
|
+
timeout: options.timeout || 30000, // 30 second default timeout
|
|
65
|
+
cwd: options.cwd || process.cwd(),
|
|
66
|
+
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
debugLog(`Executing awslocal command: ${command}`);
|
|
70
|
+
debugLog(`Environment: ${JSON.stringify(environment, null, 2)}`);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await execAsync(command, execOptions);
|
|
74
|
+
debugLog(`Command succeeded: ${command}`);
|
|
75
|
+
if (result.stdout) {
|
|
76
|
+
debugLog(`Stdout: ${result.stdout}`);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const execError = error as {
|
|
81
|
+
code?: number;
|
|
82
|
+
stdout?: string;
|
|
83
|
+
stderr?: string;
|
|
84
|
+
};
|
|
85
|
+
const exitCode = execError.code || -1;
|
|
86
|
+
const stdout = execError.stdout || '';
|
|
87
|
+
const stderr = execError.stderr || '';
|
|
88
|
+
|
|
89
|
+
debugError(`Command failed: ${command}`);
|
|
90
|
+
debugError(`Exit code: ${exitCode}`);
|
|
91
|
+
debugError(`Stdout: ${stdout}`);
|
|
92
|
+
debugError(`Stderr: ${stderr}`);
|
|
93
|
+
|
|
94
|
+
throw new AwslocalCommandError(
|
|
95
|
+
command,
|
|
96
|
+
exitCode,
|
|
97
|
+
stdout,
|
|
98
|
+
stderr,
|
|
99
|
+
environment,
|
|
100
|
+
execError as Error
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CliResult {
|
|
106
|
+
code: number;
|
|
107
|
+
error: Error | null;
|
|
108
|
+
stdout: string;
|
|
109
|
+
stderr: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TestSecret {
|
|
113
|
+
name: string;
|
|
114
|
+
value: string;
|
|
115
|
+
description?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CreatedSecret {
|
|
119
|
+
originalName: string;
|
|
120
|
+
prefixedName: string;
|
|
121
|
+
value: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class LocalStackHelper {
|
|
126
|
+
private readonly endpoint: string;
|
|
127
|
+
private readonly region: string;
|
|
128
|
+
private readonly accessKey: string;
|
|
129
|
+
private readonly secretKey: string;
|
|
130
|
+
private readonly runId: string;
|
|
131
|
+
|
|
132
|
+
constructor() {
|
|
133
|
+
this.endpoint = process.env.LOCALSTACK_URL || 'http://localhost:4566';
|
|
134
|
+
this.region = process.env.AWS_DEFAULT_REGION || 'us-east-1';
|
|
135
|
+
this.accessKey = process.env.AWS_ACCESS_KEY_ID || 'test';
|
|
136
|
+
this.secretKey = process.env.AWS_SECRET_ACCESS_KEY || 'test';
|
|
137
|
+
this.runId = generateUniqueRunId();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private getEnvironment(): Record<string, string> {
|
|
141
|
+
return {
|
|
142
|
+
AWS_ENDPOINT_URL: this.endpoint,
|
|
143
|
+
AWS_ACCESS_KEY_ID: this.accessKey,
|
|
144
|
+
AWS_SECRET_ACCESS_KEY: this.secretKey,
|
|
145
|
+
AWS_DEFAULT_REGION: this.region,
|
|
146
|
+
AWS_REGION: this.region
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async createSecret(
|
|
151
|
+
secret: TestSecret,
|
|
152
|
+
region?: string
|
|
153
|
+
): Promise<CreatedSecret> {
|
|
154
|
+
// Try to parse as JSON, if it fails, treat as plain string
|
|
155
|
+
let secretValue: string;
|
|
156
|
+
try {
|
|
157
|
+
JSON.parse(secret.value);
|
|
158
|
+
secretValue = secret.value; // Already valid JSON
|
|
159
|
+
} catch {
|
|
160
|
+
secretValue = JSON.stringify(secret.value); // Wrap plain string in quotes
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Prepend unique run ID to secret name
|
|
164
|
+
const prefixedSecretName = `${this.runId}-${secret.name}`;
|
|
165
|
+
|
|
166
|
+
const secretRegion = region || this.region;
|
|
167
|
+
const command = `awslocal secretsmanager create-secret --name "${prefixedSecretName}" --secret-string '${secretValue}' --description "${
|
|
168
|
+
secret.description || 'Test secret'
|
|
169
|
+
}" --region ${secretRegion}`;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await execAwslocalCommand(command, this.getEnvironment());
|
|
173
|
+
debugLog(`Secret created successfully: ${prefixedSecretName}`);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
originalName: secret.name,
|
|
177
|
+
prefixedName: prefixedSecretName,
|
|
178
|
+
value: secret.value,
|
|
179
|
+
description: secret.description
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof AwslocalCommandError) {
|
|
183
|
+
debugError(`Failed to create secret ${secret.name}:`);
|
|
184
|
+
debugError(`Command: ${error.command}`);
|
|
185
|
+
debugError(`Exit Code: ${error.exitCode}`);
|
|
186
|
+
debugError(`Stdout: ${error.stdout}`);
|
|
187
|
+
debugError(`Stderr: ${error.stderr}`);
|
|
188
|
+
debugError(
|
|
189
|
+
`Environment: ${JSON.stringify(error.environment, null, 2)}`
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Provide helpful debugging information
|
|
193
|
+
if (error.stderr.includes('already exists')) {
|
|
194
|
+
debugLog(
|
|
195
|
+
`Hint: Secret '${prefixedSecretName}' may already exist. Try deleting it first or use a different name.`
|
|
196
|
+
);
|
|
197
|
+
} else if (
|
|
198
|
+
error.stderr.includes('connection') ||
|
|
199
|
+
error.stderr.includes('timeout')
|
|
200
|
+
) {
|
|
201
|
+
debugLog(`Hint: Check if LocalStack is running at ${this.endpoint}`);
|
|
202
|
+
debugLog(`Hint: Try running: awslocal sts get-caller-identity`);
|
|
203
|
+
} else if (
|
|
204
|
+
error.stderr.includes('not found') ||
|
|
205
|
+
error.stderr.includes('command not found')
|
|
206
|
+
) {
|
|
207
|
+
debugLog(
|
|
208
|
+
`Hint: Make sure awslocal is installed: pip install awscli-local`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
debugLog(
|
|
213
|
+
`Unexpected error creating secret ${prefixedSecretName}:`,
|
|
214
|
+
error
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async deleteSecret(secretName: string): Promise<void> {
|
|
222
|
+
// Prepend unique run ID to secret name if not already prefixed
|
|
223
|
+
const prefixedSecretName = secretName.startsWith(`${this.runId}-`)
|
|
224
|
+
? secretName
|
|
225
|
+
: `${this.runId}-${secretName}`;
|
|
226
|
+
|
|
227
|
+
const command = `awslocal secretsmanager delete-secret --secret-id "${prefixedSecretName}" --force-delete-without-recovery --region ${this.region}`;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await execAwslocalCommand(command, this.getEnvironment());
|
|
231
|
+
debugLog(`Secret deleted successfully: ${prefixedSecretName}`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof AwslocalCommandError) {
|
|
234
|
+
// For cleanup operations, we log warnings but don't throw unless it's critical
|
|
235
|
+
debugWarn(`Failed to delete secret ${prefixedSecretName}:`);
|
|
236
|
+
debugWarn(`Command: ${error.command}`);
|
|
237
|
+
debugWarn(`Exit Code: ${error.exitCode}`);
|
|
238
|
+
debugWarn(`Stdout: ${error.stdout}`);
|
|
239
|
+
debugWarn(`Stderr: ${error.stderr}`);
|
|
240
|
+
|
|
241
|
+
// Only throw for critical errors (not "not found" errors)
|
|
242
|
+
if (
|
|
243
|
+
!error.stderr.includes('not found') &&
|
|
244
|
+
!error.stderr.includes('does not exist')
|
|
245
|
+
) {
|
|
246
|
+
debugLog(`Critical error during cleanup - rethrowing`);
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
debugLog(
|
|
251
|
+
`Unexpected error deleting secret ${prefixedSecretName}:`,
|
|
252
|
+
error
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async cleanupRunSecrets(): Promise<void> {
|
|
259
|
+
debugLog(`Cleaning up secrets with prefix: ${this.runId}`);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const allSecrets = await this.listSecrets();
|
|
263
|
+
const runSecrets = allSecrets.filter((secretName) =>
|
|
264
|
+
secretName.startsWith(`${this.runId}-`)
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
debugLog(
|
|
268
|
+
`Found ${runSecrets.length} secrets to cleanup for run ${this.runId}`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Delete all secrets with the run prefix
|
|
272
|
+
for (const secretName of runSecrets) {
|
|
273
|
+
try {
|
|
274
|
+
await this.deleteSecret(secretName);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
debugWarn(`Failed to cleanup secret ${secretName}:`, error);
|
|
277
|
+
// Continue with other secrets even if one fails
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
debugLog(`Cleanup completed for run ${this.runId}`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
debugWarn(`Failed to cleanup secrets for run ${this.runId}:`, error);
|
|
284
|
+
// Don't throw - cleanup failures shouldn't break tests
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async listSecrets(): Promise<string[]> {
|
|
289
|
+
const command = `awslocal secretsmanager list-secrets --region ${this.region}`;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const result = await execAwslocalCommand(command, this.getEnvironment());
|
|
293
|
+
const parsedResult = JSON.parse(result.stdout);
|
|
294
|
+
return (
|
|
295
|
+
parsedResult.SecretList?.map(
|
|
296
|
+
(secret: { Name: string }) => secret.Name
|
|
297
|
+
) || []
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error instanceof AwslocalCommandError) {
|
|
301
|
+
debugError('Failed to list secrets:');
|
|
302
|
+
debugError(`Command: ${error.command}`);
|
|
303
|
+
debugError(`Exit Code: ${error.exitCode}`);
|
|
304
|
+
debugError(`Stdout: ${error.stdout}`);
|
|
305
|
+
debugError(`Stderr: ${error.stderr}`);
|
|
306
|
+
debugError(
|
|
307
|
+
`Environment: ${JSON.stringify(error.environment, null, 2)}`
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Provide helpful debugging information
|
|
311
|
+
if (
|
|
312
|
+
error.stderr.includes('connection') ||
|
|
313
|
+
error.stderr.includes('timeout')
|
|
314
|
+
) {
|
|
315
|
+
debugLog(`Hint: Check if LocalStack is running at ${this.endpoint}`);
|
|
316
|
+
debugLog(`Hint: Try running: awslocal sts get-caller-identity`);
|
|
317
|
+
} else if (
|
|
318
|
+
error.stderr.includes('not found') ||
|
|
319
|
+
error.stderr.includes('command not found')
|
|
320
|
+
) {
|
|
321
|
+
debugLog(
|
|
322
|
+
`Hint: Make sure awslocal is installed: pip install awscli-local`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
debugLog('Unexpected error listing secrets:', error);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Return empty array for non-critical errors to allow tests to continue
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async waitForLocalStack(): Promise<void> {
|
|
335
|
+
const maxRetries = 30;
|
|
336
|
+
const retryDelay = 1000; // 1 second
|
|
337
|
+
|
|
338
|
+
debugLog(`Waiting for LocalStack at ${this.endpoint}...`);
|
|
339
|
+
|
|
340
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
341
|
+
try {
|
|
342
|
+
const command = `awslocal sts get-caller-identity --region ${this.region}`;
|
|
343
|
+
debugLog(`Testing LocalStack connectivity with: ${command}`);
|
|
344
|
+
|
|
345
|
+
const result = await execAwslocalCommand(
|
|
346
|
+
command,
|
|
347
|
+
this.getEnvironment()
|
|
348
|
+
);
|
|
349
|
+
debugLog('LocalStack is ready');
|
|
350
|
+
if (result.stdout) {
|
|
351
|
+
debugLog(`LocalStack response: ${result.stdout}`);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (i === maxRetries - 1) {
|
|
356
|
+
debugError('LocalStack failed to start within timeout');
|
|
357
|
+
|
|
358
|
+
if (error instanceof AwslocalCommandError) {
|
|
359
|
+
debugError(`Last command: ${error.command}`);
|
|
360
|
+
debugError(`Last exit code: ${error.exitCode}`);
|
|
361
|
+
debugError(`Last stdout: ${error.stdout}`);
|
|
362
|
+
debugError(`Last stderr: ${error.stderr}`);
|
|
363
|
+
debugError(
|
|
364
|
+
`Environment: ${JSON.stringify(error.environment, null, 2)}`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Provide comprehensive debugging information
|
|
368
|
+
debugLog('\n=== LocalStack Debugging Information ===');
|
|
369
|
+
debugLog(`Endpoint: ${this.endpoint}`);
|
|
370
|
+
debugLog(`Region: ${this.region}`);
|
|
371
|
+
debugLog(`Access Key: ${this.accessKey}`);
|
|
372
|
+
debugLog(`Secret Key: ${this.secretKey}`);
|
|
373
|
+
debugLog('\n=== Troubleshooting Steps ===');
|
|
374
|
+
debugLog('1. Check if LocalStack is running:');
|
|
375
|
+
debugLog(' docker-compose ps');
|
|
376
|
+
debugLog('2. Check LocalStack logs:');
|
|
377
|
+
debugLog(' docker-compose logs localstack');
|
|
378
|
+
debugLog('3. Test awslocal installation:');
|
|
379
|
+
debugLog(' awslocal --version');
|
|
380
|
+
debugLog('4. Test direct connectivity:');
|
|
381
|
+
debugLog(` curl ${this.endpoint}/health`);
|
|
382
|
+
debugLog('5. Check if port 4566 is accessible:');
|
|
383
|
+
debugLog(' netstat -tlnp | grep 4566');
|
|
384
|
+
} else {
|
|
385
|
+
debugLog(`Last error: ${error.message}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
throw new Error('LocalStack failed to start within timeout');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
debugLog(`Waiting for LocalStack... (${i + 1}/${maxRetries})`);
|
|
392
|
+
if (error instanceof AwslocalCommandError && error.stderr) {
|
|
393
|
+
debugLog(`Error: ${error.stderr}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function cli(args: string[], cwd = '.'): Promise<CliResult> {
|
|
403
|
+
return new Promise((resolve) => {
|
|
404
|
+
// Clean environment by removing AWS variables that might interfere
|
|
405
|
+
const cleanEnv = { ...process.env };
|
|
406
|
+
delete cleanEnv.AWS_PROFILE;
|
|
407
|
+
delete cleanEnv.AWS_DEFAULT_PROFILE;
|
|
408
|
+
delete cleanEnv.AWS_SESSION_TOKEN;
|
|
409
|
+
delete cleanEnv.AWS_SECURITY_TOKEN;
|
|
410
|
+
delete cleanEnv.AWS_ROLE_ARN;
|
|
411
|
+
delete cleanEnv.AWS_ROLE_SESSION_NAME;
|
|
412
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
|
|
413
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
|
|
414
|
+
|
|
415
|
+
// Default environment variables for LocalStack
|
|
416
|
+
const defaultEnv = {
|
|
417
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
418
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
419
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
420
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
421
|
+
AWS_REGION: 'us-east-1',
|
|
422
|
+
NODE_ENV: 'test'
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const envVars = { ...cleanEnv, ...defaultEnv };
|
|
426
|
+
const command = `node ${path.resolve('./dist/index')} ${args.join(' ')}`;
|
|
427
|
+
|
|
428
|
+
debugLog(`Running CLI command: ${command}`);
|
|
429
|
+
debugLog(
|
|
430
|
+
`Environment: AWS_ENDPOINT_URL=${envVars.AWS_ENDPOINT_URL}, AWS_DEFAULT_REGION=${envVars.AWS_DEFAULT_REGION}`
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
exec(command, { cwd, env: envVars }, (error, stdout, stderr) => {
|
|
434
|
+
const result = {
|
|
435
|
+
code: error && error.code ? error.code : 0,
|
|
436
|
+
error: error || null,
|
|
437
|
+
stdout,
|
|
438
|
+
stderr
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
if (result.code !== 0) {
|
|
442
|
+
debugError(`CLI command failed with code ${result.code}`);
|
|
443
|
+
debugError(`Command: ${command}`);
|
|
444
|
+
debugError(`Stdout: ${result.stdout}`);
|
|
445
|
+
debugError(`Stderr: ${result.stderr}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
resolve(result);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function cliWithEnv(
|
|
454
|
+
args: string[],
|
|
455
|
+
env: Record<string, string>,
|
|
456
|
+
cwd = '.'
|
|
457
|
+
): Promise<CliResult> {
|
|
458
|
+
return new Promise((resolve) => {
|
|
459
|
+
// Clean environment by removing AWS variables that might interfere
|
|
460
|
+
const cleanEnv = { ...process.env };
|
|
461
|
+
delete cleanEnv.AWS_PROFILE;
|
|
462
|
+
delete cleanEnv.AWS_DEFAULT_PROFILE;
|
|
463
|
+
delete cleanEnv.AWS_SESSION_TOKEN;
|
|
464
|
+
delete cleanEnv.AWS_SECURITY_TOKEN;
|
|
465
|
+
delete cleanEnv.AWS_ROLE_ARN;
|
|
466
|
+
delete cleanEnv.AWS_ROLE_SESSION_NAME;
|
|
467
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
|
|
468
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
|
|
469
|
+
|
|
470
|
+
// Default environment variables for LocalStack
|
|
471
|
+
const defaultEnv = {
|
|
472
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
473
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
474
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
475
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
476
|
+
AWS_REGION: 'us-east-1',
|
|
477
|
+
NODE_ENV: 'test'
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const envVars = { ...cleanEnv, ...defaultEnv, ...env };
|
|
481
|
+
const command = `node ${path.resolve('./dist/index')} ${args.join(' ')}`;
|
|
482
|
+
|
|
483
|
+
debugLog(`Running CLI command with custom env: ${command}`);
|
|
484
|
+
debugLog(
|
|
485
|
+
`Environment: AWS_ENDPOINT_URL=${envVars.AWS_ENDPOINT_URL}, AWS_DEFAULT_REGION=${envVars.AWS_DEFAULT_REGION}`
|
|
486
|
+
);
|
|
487
|
+
debugLog(`Custom env vars:`, Object.keys(env));
|
|
488
|
+
|
|
489
|
+
exec(command, { cwd, env: envVars }, (error, stdout, stderr) => {
|
|
490
|
+
const result = {
|
|
491
|
+
code: error && error.code ? error.code : 0,
|
|
492
|
+
error: error || null,
|
|
493
|
+
stdout,
|
|
494
|
+
stderr
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (result.code !== 0) {
|
|
498
|
+
debugError(`CLI command failed with code ${result.code}`);
|
|
499
|
+
debugError(`Command: ${command}`);
|
|
500
|
+
debugError(`Stdout: ${result.stdout}`);
|
|
501
|
+
debugError(`Stderr: ${result.stderr}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
resolve(result);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function createTempFile(content: string) {
|
|
510
|
+
const tempDir = os.tmpdir();
|
|
511
|
+
const tempFile = path.join(tempDir, `env-secrets-test-${Date.now()}.env`);
|
|
512
|
+
fs.writeFileSync(tempFile, content);
|
|
513
|
+
return tempFile;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function cleanupTempFile(filePath: string): void {
|
|
517
|
+
try {
|
|
518
|
+
if (fs.existsSync(filePath)) {
|
|
519
|
+
fs.unlinkSync(filePath);
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
debugWarn(`Failed to cleanup temp file ${filePath}:`, error);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function createTestProfile() {
|
|
527
|
+
const homeDir = os.homedir();
|
|
528
|
+
const awsDir = path.join(homeDir, '.aws');
|
|
529
|
+
const credentialsFile = path.join(awsDir, 'credentials');
|
|
530
|
+
const configFile = path.join(awsDir, 'config');
|
|
531
|
+
|
|
532
|
+
// Create .aws directory if it doesn't exist
|
|
533
|
+
if (!fs.existsSync(awsDir)) {
|
|
534
|
+
fs.mkdirSync(awsDir, { mode: 0o700 });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Backup existing files if they exist
|
|
538
|
+
const backupCredentials = credentialsFile + '.backup';
|
|
539
|
+
const backupConfig = configFile + '.backup';
|
|
540
|
+
|
|
541
|
+
if (fs.existsSync(credentialsFile)) {
|
|
542
|
+
fs.copyFileSync(credentialsFile, backupCredentials);
|
|
543
|
+
}
|
|
544
|
+
if (fs.existsSync(configFile)) {
|
|
545
|
+
fs.copyFileSync(configFile, backupConfig);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Create test profile
|
|
549
|
+
const credentialsContent = `[default]
|
|
550
|
+
aws_access_key_id = test
|
|
551
|
+
aws_secret_access_key = test
|
|
552
|
+
|
|
553
|
+
[env-secrets-test]
|
|
554
|
+
aws_access_key_id = test
|
|
555
|
+
aws_secret_access_key = test
|
|
556
|
+
`;
|
|
557
|
+
|
|
558
|
+
const configContent = `[default]
|
|
559
|
+
region = us-east-1
|
|
560
|
+
|
|
561
|
+
[profile env-secrets-test]
|
|
562
|
+
region = us-east-1
|
|
563
|
+
`;
|
|
564
|
+
|
|
565
|
+
fs.writeFileSync(credentialsFile, credentialsContent, { mode: 0o600 });
|
|
566
|
+
fs.writeFileSync(configFile, configContent, { mode: 0o600 });
|
|
567
|
+
|
|
568
|
+
return awsDir;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function restoreTestProfile(awsDir: string | undefined): void {
|
|
572
|
+
if (!awsDir) {
|
|
573
|
+
debugWarn('No AWS directory provided for profile restoration');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const credentialsFile = path.join(awsDir, 'credentials');
|
|
578
|
+
const configFile = path.join(awsDir, 'config');
|
|
579
|
+
const backupCredentials = credentialsFile + '.backup';
|
|
580
|
+
const backupConfig = configFile + '.backup';
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
if (fs.existsSync(backupCredentials)) {
|
|
584
|
+
fs.copyFileSync(backupCredentials, credentialsFile);
|
|
585
|
+
fs.unlinkSync(backupCredentials);
|
|
586
|
+
} else if (fs.existsSync(credentialsFile)) {
|
|
587
|
+
fs.unlinkSync(credentialsFile);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (fs.existsSync(backupConfig)) {
|
|
591
|
+
fs.copyFileSync(backupConfig, configFile);
|
|
592
|
+
fs.unlinkSync(backupConfig);
|
|
593
|
+
} else if (fs.existsSync(configFile)) {
|
|
594
|
+
fs.unlinkSync(configFile);
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
debugWarn('Failed to restore AWS profile:', error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function checkAwslocalInstalled(): Promise<void> {
|
|
602
|
+
try {
|
|
603
|
+
await execAwslocalCommand('awslocal --version', {});
|
|
604
|
+
debugLog('awslocal is installed and available');
|
|
605
|
+
} catch (error) {
|
|
606
|
+
debugLog('awslocal is not installed or not available in PATH');
|
|
607
|
+
debugLog('');
|
|
608
|
+
|
|
609
|
+
if (error instanceof AwslocalCommandError) {
|
|
610
|
+
debugError(`Command: ${error.command}`);
|
|
611
|
+
debugError(`Exit Code: ${error.exitCode}`);
|
|
612
|
+
debugError(`Stdout: ${error.stdout}`);
|
|
613
|
+
debugError(`Stderr: ${error.stderr}`);
|
|
614
|
+
} else {
|
|
615
|
+
debugError(`Error: ${error.message}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
debugLog('');
|
|
619
|
+
debugLog('Please install awslocal by running:');
|
|
620
|
+
debugLog(' pip install awscli-local');
|
|
621
|
+
debugLog('');
|
|
622
|
+
debugLog('Or using npm:');
|
|
623
|
+
debugLog(' npm install -g awscli-local');
|
|
624
|
+
debugLog('');
|
|
625
|
+
debugLog(
|
|
626
|
+
'For more information, visit: https://github.com/localstack/awscli-local'
|
|
627
|
+
);
|
|
628
|
+
debugLog('');
|
|
629
|
+
debugLog('=== Troubleshooting Steps ===');
|
|
630
|
+
debugLog('1. Verify Python/pip is installed:');
|
|
631
|
+
debugLog(' python --version');
|
|
632
|
+
debugLog(' pip --version');
|
|
633
|
+
debugLog('2. Try installing with sudo if needed:');
|
|
634
|
+
debugLog(' sudo pip install awscli-local');
|
|
635
|
+
debugLog('3. Check if awslocal is in your PATH:');
|
|
636
|
+
debugLog(' which awslocal');
|
|
637
|
+
debugLog(' echo $PATH');
|
|
638
|
+
debugLog('4. Try using the full path:');
|
|
639
|
+
debugLog(' /usr/local/bin/awslocal --version');
|
|
640
|
+
|
|
641
|
+
throw new Error(
|
|
642
|
+
'awslocal is required for end-to-end tests but is not installed'
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|