-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Feature: Preserve Existing
.env
File and Update Values if Present (…
…#5)
- Loading branch information
Showing
6 changed files
with
510 additions
and
405 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,88 @@ | ||
import * as fs from 'node:fs'; | ||
import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm'; | ||
import * as dotenv from 'dotenv'; | ||
|
||
const ssm = new SSM({}); | ||
|
||
export async function run(mapPath: string, envFilePath: string) { | ||
const paramMap = loadParamMap(mapPath); | ||
const existingEnvVariables = loadExistingEnvVariables(envFilePath); | ||
|
||
const updatedEnvVariables = await fetchAndUpdateEnvVariables(paramMap, existingEnvVariables); | ||
|
||
writeEnvFile(envFilePath, updatedEnvVariables); | ||
console.log(`Environment File generated at '${envFilePath}'`); | ||
} | ||
|
||
function loadParamMap(mapPath: string): Record<string, string> { | ||
const content = fs.readFileSync(mapPath, 'utf-8'); | ||
const paramMap: Record<string, string> = JSON.parse(content); | ||
try { | ||
return JSON.parse(content); | ||
} catch (error) { | ||
console.error(`Error parsing JSON from ${mapPath}`); | ||
throw new Error(`Invalid JSON in parameter map file: ${mapPath}`); | ||
} | ||
} | ||
|
||
function loadExistingEnvVariables(envFilePath: string): Record<string, string> { | ||
const envVariables: Record<string, string> = {}; | ||
|
||
if (!fs.existsSync(envFilePath)) return envVariables; | ||
|
||
const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8'); | ||
const parsedEnv = dotenv.parse(existingEnvContent); | ||
Object.assign(envVariables, parsedEnv); | ||
|
||
return envVariables; | ||
} | ||
|
||
const envContent: string[] = []; | ||
async function fetchAndUpdateEnvVariables( | ||
paramMap: Record<string, string>, | ||
existingEnvVariables: Record<string, string>, | ||
): Promise<Record<string, string>> { | ||
const errors: string[] = []; | ||
|
||
console.log('Fetching parameters...'); | ||
for (const [envVar, ssmName] of Object.entries(paramMap)) { | ||
try { | ||
const command = new GetParameterCommand({ | ||
Name: ssmName, | ||
WithDecryption: true, | ||
}); | ||
|
||
const { Parameter } = await ssm.send(command); | ||
const value = Parameter?.Value; | ||
const value = await fetchSSMParameter(ssmName); | ||
if (value) { | ||
envContent.push(`${envVar}=${value}`); | ||
console.log(`${envVar}=${value}`); | ||
existingEnvVariables[envVar] = value; | ||
console.log( | ||
`${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`, | ||
); | ||
} else { | ||
console.error(`Warning: No value found for ${ssmName}`); | ||
console.error(`Warning: No value found for: '${ssmName}'`); | ||
} | ||
} catch (error) { | ||
console.error(`Error fetching parameter ${ssmName}: ${error}`); | ||
throw new Error(`ParameterNotFound: ${ssmName}`); | ||
console.error(`Error fetching parameter: '${ssmName}'`); | ||
errors.push(`ParameterNotFound: ${ssmName}`); | ||
} | ||
} | ||
|
||
fs.writeFileSync(envFilePath, envContent.join('\n')); | ||
console.log(`.env file generated at ${envFilePath}`); | ||
if (errors.length > 0) { | ||
throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`); | ||
} | ||
|
||
return existingEnvVariables; | ||
} | ||
|
||
async function fetchSSMParameter(ssmName: string): Promise<string | undefined> { | ||
const command = new GetParameterCommand({ | ||
Name: ssmName, | ||
WithDecryption: true, | ||
}); | ||
|
||
const { Parameter } = await ssm.send(command); | ||
return Parameter?.Value; | ||
} | ||
|
||
function writeEnvFile(envFilePath: string, envVariables: Record<string, string>): void { | ||
const envContent = Object.entries(envVariables) | ||
.map(([key, value]) => { | ||
const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"'); | ||
return `${key}=${escapedValue}`; | ||
}) | ||
.join('\n'); | ||
|
||
fs.writeFileSync(envFilePath, envContent); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,12 +11,17 @@ vi.mock('@aws-sdk/client-ssm', () => { | |
Parameter: { Value: '[email protected]' }, | ||
}); | ||
} | ||
|
||
if (command.input.Name === '/path/to/ssm/password') { | ||
return Promise.resolve({ | ||
Parameter: { Value: 'mockedPassword' }, | ||
}); | ||
} | ||
|
||
if (command.input.Name === '/path/to/ssm/password_no_value') { | ||
return Promise.resolve({ Parameter: { Value: '' } }); | ||
} | ||
|
||
return Promise.reject(new Error(`ParameterNotFound: ${command.input.Name}`)); | ||
}), | ||
})), | ||
|
@@ -57,15 +62,82 @@ describe('Envilder CLI', () => { | |
const mockMapPath = './tests/param_map.json'; | ||
const mockEnvFilePath = './tests/.env.test'; | ||
const paramMapContent = { | ||
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/unknown-email', // Non-existent parameter | ||
NEXT_PUBLIC_CREDENTIAL_EMAIL: 'non-existent parameter', | ||
}; | ||
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent)); | ||
|
||
// Act | ||
const action = run(mockMapPath, mockEnvFilePath); | ||
|
||
// Assert | ||
await expect(action).rejects.toThrow('ParameterNotFound: /path/to/ssm/unknown-email'); | ||
await expect(action).rejects.toThrow('ParameterNotFound: non-existent parameter'); | ||
fs.unlinkSync(mockMapPath); | ||
}); | ||
|
||
it('Should_AppendNewSSMParameters_When_EnvFileContainsExistingVariables', async () => { | ||
// Arrange | ||
const mockMapPath = './tests/param_map.json'; | ||
const mockEnvFilePath = './tests/.env.test'; | ||
|
||
const existingEnvContent = 'EXISTING_VAR=existingValue'; | ||
fs.writeFileSync(mockEnvFilePath, existingEnvContent); | ||
const paramMapContent = { | ||
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email', | ||
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password', | ||
}; | ||
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent)); | ||
|
||
// Act | ||
await run(mockMapPath, mockEnvFilePath); | ||
|
||
// Assert | ||
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8'); | ||
expect(updatedEnvFileContent).toContain('EXISTING_VAR=existingValue'); | ||
expect(updatedEnvFileContent).toContain('[email protected]'); | ||
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword'); | ||
fs.unlinkSync(mockEnvFilePath); | ||
fs.unlinkSync(mockMapPath); | ||
}); | ||
|
||
it('Should_OverwriteSSMParameters_When_EnvFileContainsSameVariables', async () => { | ||
// Arrange | ||
const mockMapPath = './tests/param_map.json'; | ||
const mockEnvFilePath = './tests/.env.test'; | ||
const existingEnvContent = '[email protected]'; | ||
fs.writeFileSync(mockEnvFilePath, existingEnvContent); | ||
const paramMapContent = { | ||
NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email', | ||
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password', | ||
}; | ||
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent)); | ||
|
||
// Act | ||
await run(mockMapPath, mockEnvFilePath); | ||
|
||
// Assert | ||
const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8'); | ||
expect(updatedEnvFileContent).toContain('[email protected]'); | ||
expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword'); | ||
fs.unlinkSync(mockEnvFilePath); | ||
fs.unlinkSync(mockMapPath); | ||
}); | ||
|
||
it('Should_LogWarning_When_SSMParameterHasNoValue', async () => { | ||
// Arrange | ||
const mockMapPath = './tests/param_map.json'; | ||
const mockEnvFilePath = './tests/.env.test'; | ||
const paramMapContent = { | ||
NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password_no_value', | ||
}; | ||
fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent)); | ||
const consoleSpy = vi.spyOn(console, 'error'); | ||
|
||
// Act | ||
await run(mockMapPath, mockEnvFilePath); | ||
|
||
// Assert | ||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Warning: No value found for')); | ||
fs.unlinkSync(mockEnvFilePath); | ||
fs.unlinkSync(mockMapPath); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
{ | ||
"NEXT_PUBLIC_CREDENTIAL_EMAIL": "/M47.Claims.Apps.Minimal.Api/Production/Auth/CredentialEmail", | ||
"NEXT_PUBLIC_CREDENTIAL_PASSWORD": "/M47.Claims.Apps.Minimal.Api/Production/Auth/CredentialPassword", | ||
"NEXT_PUBLIC_JWT_SECRET": "/M47.Claims.Apps.Minimal.Api/Production/Auth/JwtSecret" | ||
"TOKEN_SECRET": "/Test/Token" | ||
} |
Oops, something went wrong.