From 96e54413965474c6bb190d7a4f21fc4caedd310f Mon Sep 17 00:00:00 2001 From: Olivier Hoareau Date: Mon, 2 Sep 2024 00:04:59 +0200 Subject: [PATCH] feat: add support for multi-regions --- src/types.ts | 14 ++++++++ src/utils/fetchConfig.ts | 8 +++++ src/utils/fetchLayers.ts | 16 +++++++++ src/utils/generateEnvLayerFromFile.ts | 46 ++++++++++++++++++------ src/utils/generateEnvLayers.ts | 51 +++++++++++++++++++++++++++ src/utils/tfgen.ts | 51 +++++---------------------- test/tflayer.spec.ts | 2 +- 7 files changed, 135 insertions(+), 53 deletions(-) create mode 100644 src/utils/fetchConfig.ts create mode 100644 src/utils/fetchLayers.ts create mode 100644 src/utils/generateEnvLayers.ts diff --git a/src/types.ts b/src/types.ts index 84ddb7e..bb3c0a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,12 @@ export type config = { export type layer_config = { only_on_envs?: string[]; not_on_envs?: string[]; + envs?: Record; +}; + +export type enriched_layer_config = { + defaultRegion: string; + regions?: string[]; }; export type loggable = { @@ -61,3 +67,11 @@ export type fetch_layer = ( env: string, name: string, ) => Promise<[Record, string]>; + +export type fetched_layer = { + name: string; + file: string; + filePath: string; +}; + +export type fetch_config = (configFile: string) => Promise; diff --git a/src/utils/fetchConfig.ts b/src/utils/fetchConfig.ts new file mode 100644 index 0000000..d341456 --- /dev/null +++ b/src/utils/fetchConfig.ts @@ -0,0 +1,8 @@ +import {resolve} from 'path'; +import {fetch_config} from '../types'; + +export const fetchConfig: fetch_config = async (configFile: string) => { + return import(resolve(configFile)); +}; + +export default fetchConfig; diff --git a/src/utils/fetchLayers.ts b/src/utils/fetchLayers.ts new file mode 100644 index 0000000..357f2b2 --- /dev/null +++ b/src/utils/fetchLayers.ts @@ -0,0 +1,16 @@ +import {readdirSync} from 'fs'; +import {fetched_layer} from '../types'; + +export const fetchLayers = async ( + sourceDir: string, +): Promise => { + return readdirSync(sourceDir, {withFileTypes: true}) + .filter(e => !e.isDirectory() && /.tmpl.tf$/.test(e.name)) + .map(e => ({ + name: e.name.replace(/\.tmpl\.tf$/, ''), + file: e.name, + filePath: `${sourceDir}/${e.name}`, + })); +}; + +export default fetchLayers; diff --git a/src/utils/generateEnvLayerFromFile.ts b/src/utils/generateEnvLayerFromFile.ts index f54263c..4d34a4e 100644 --- a/src/utils/generateEnvLayerFromFile.ts +++ b/src/utils/generateEnvLayerFromFile.ts @@ -1,21 +1,47 @@ -import {dirname} from 'path'; import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs'; import replaceVars from './replaceVars'; +import {enriched_layer_config} from '../types'; export const generateEnvLayerFromFile = async ( sourceFile: string, - targetFile: string, + targetDir: string, vars: Record, + layerConfig: enriched_layer_config, ) => { - const parentDir = dirname(targetFile); - existsSync(parentDir) || mkdirSync(parentDir, {recursive: true}); - writeFileSync( - targetFile, - replaceVars( - readFileSync(sourceFile, 'utf8') as string, - vars, - ) as unknown as string, + existsSync(targetDir) || mkdirSync(targetDir, {recursive: true}); + + const defaultRegion = layerConfig.defaultRegion; + const regions: string[] = layerConfig.regions || []; + + const mappedRegions = (regions || [defaultRegion]).map( + r => + [ + r, + `${targetDir}/main${r === defaultRegion ? '' : `_${r.replace(/-/g, '_')}`}.tf`, + ] as [string, string], + ); + + const reports = await Promise.allSettled( + mappedRegions.map(async ([r, targetFile]: [string, string]) => { + writeFileSync( + targetFile, + replaceVars(readFileSync(sourceFile, 'utf8') as string, { + ...vars, + region: r, + rsuffix: r === defaultRegion ? '' : `-${r}`, + }) as unknown as string, + ); + }), ); + + const errors: {reason: Error}[] = reports.filter( + x => x.status !== 'fulfilled', + ) as unknown as {reason: Error}[]; + + if (errors.length) + throw new Error( + `Unable to generate all env layer files for ${sourceFile}: ${errors.map((x: {reason: Error}) => x.reason.message).join('\n')}`, + ); }; export default generateEnvLayerFromFile; diff --git a/src/utils/generateEnvLayers.ts b/src/utils/generateEnvLayers.ts new file mode 100644 index 0000000..3747ff0 --- /dev/null +++ b/src/utils/generateEnvLayers.ts @@ -0,0 +1,51 @@ +import mergeEnvConfig from './mergeEnvConfig'; +import generateEnvLayerFromFile from './generateEnvLayerFromFile'; +import generateLayerVars from './generateLayerVars'; +import {fetched_layer, config, enriched_layer_config} from '../types'; + +const defaultRegion = 'eu-west-3'; + +export const generateEnvLayers = async ( + layers: fetched_layer[], + config: config, + targetDir: string, + defaultLayerConfig: Record = {}, +) => { + return Promise.all( + Object.keys(config.environments || {}).map(async (name: string) => { + const env = mergeEnvConfig(config, name); + await Promise.all( + layers.map(async ({name: layer, file, filePath}) => { + const layerEnv = {...env, env: name, layer: layer}; + const layerConfig = + (config && config.layers && config.layers[layer]) || {}; + if (layerConfig) { + if ( + layerConfig.only_on_envs && + !layerConfig.only_on_envs.includes(name) + ) + return; + if ( + layerConfig.not_on_envs && + layerConfig.not_on_envs.includes(name) + ) + return; + } + await generateEnvLayerFromFile( + filePath, + `${targetDir}/${name}/${file.replace(/\.tmpl\.tf$/, '')}`, + generateLayerVars(layerEnv, layerEnv), + { + defaultRegion, + ...defaultLayerConfig, + ...(layerConfig?.['envs']?.['common'] || {}), + ...(layerConfig?.['envs']?.[name] || {}), + } as enriched_layer_config, + ); + }), + ); + }), + ); +}; + +export default generateEnvLayers; diff --git a/src/utils/tfgen.ts b/src/utils/tfgen.ts index cbca9b0..b1df1c8 100644 --- a/src/utils/tfgen.ts +++ b/src/utils/tfgen.ts @@ -1,51 +1,18 @@ -import {readdirSync} from 'fs'; -import {resolve} from 'path'; -import mergeEnvConfig from './mergeEnvConfig'; -import generateEnvLayerFromFile from './generateEnvLayerFromFile'; -import generateLayerVars from './generateLayerVars'; -import {config} from '../types'; +import fetchLayers from './fetchLayers'; +import fetchConfig from './fetchConfig'; +import generateEnvLayers from './generateEnvLayers'; export const tfgen = async ( configFile: string, sourceDir: string, targetDir: string, + defaultLayerConfig?: Record, ) => { - const layers = readdirSync(sourceDir, {withFileTypes: true}) - .filter(e => !e.isDirectory() && /.tmpl.tf$/.test(e.name)) - .map(e => ({ - name: e.name.replace(/\.tmpl\.tf$/, ''), - file: e.name, - filePath: `${sourceDir}/${e.name}`, - })); - const config: config = await import(resolve(configFile)); - await Promise.all( - Object.keys(config.environments || {}).map(async (name: string) => { - const env = mergeEnvConfig(config, name); - await Promise.all( - layers.map(async ({name: layer, file, filePath}) => { - const layerEnv = {...env, env: name, layer: layer}; - const layerConfig = - (config && config.layers && config.layers[layer]) || {}; - if (layerConfig) { - if ( - layerConfig.only_on_envs && - !layerConfig.only_on_envs.includes(name) - ) - return; - if ( - layerConfig.not_on_envs && - layerConfig.not_on_envs.includes(name) - ) - return; - } - await generateEnvLayerFromFile( - filePath, - `${targetDir}/${name}/${file.replace(/\.tmpl\.tf$/, '')}/main.tf`, - generateLayerVars(layerEnv, layerEnv), - ); - }), - ); - }), + await generateEnvLayers( + await fetchLayers(sourceDir), + await fetchConfig(configFile), + targetDir, + defaultLayerConfig, ); }; diff --git a/test/tflayer.spec.ts b/test/tflayer.spec.ts index 43d51c1..3eb78b0 100644 --- a/test/tflayer.spec.ts +++ b/test/tflayer.spec.ts @@ -6,7 +6,7 @@ import getDirectories from '../src/utils/getDirectories'; beforeEach(() => { jest.resetAllMocks(); }) -describe('tflayer', () => { +describe('list-layers', () => { it('no directories outputs empty string', async () => { const logger = jest.fn(); (getDirectories as jest.Mock).mockImplementation(() => []);