/**
* This tool is only used for LOCAL DEVELOPMENT purposes. With this approach we don't the separate config
* files per environment anymore. Just change your CONFIG_ENV to get the correct app settings
* variables
*
* The possible CONFIG_ENV: dev, pen, tea, teb, teg, tst
*/
import path from 'path';
import fs from 'fs';
import merge from 'lodash/merge';
import debugLib from 'debug';
import { AppConfigurationClient } from '@azure/app-configuration';
import { SecretClient } from '@azure/keyvault-secrets';
import {
DefaultAzureCredential,
ChainedTokenCredential,
ManagedIdentityCredential,
} from '@azure/identity';
import environmentConfig from './config/configuration.json';
import { labelFilterObject } from './AzureOptions';
const MATCH_UPPERCASE_UNDERSCORE_VALUES = /\b[A-Z0-9]+(?:_[A-Z0-9]+)+\b/g;
const KEYVAULT_URI_REGEX = /(vault.azure.net\/secrets)|([^/]+)\/?$/g;
// For the app settings
const client = new AppConfigurationClient(
process.env.APPCONFIG_ENDPOINT,
new DefaultAzureCredential(),
);
// For the keyVault settings
const credential = new ChainedTokenCredential(
new DefaultAzureCredential(),
new ManagedIdentityCredential(),
);
const stringifyConfig = JSON.stringify(environmentConfig);
const debug = debugLib('SlimmingWorld:getEnvironmentConfig');
/**
*
* @returns {Promise<boolean>}
*/
export const getEnvironmentConfig = async callback => {
/**
* This is for BE to have the ability to override the configuration settings for feature development
* @type {null}
*/
let localBeConfig = null;
const fullPath = path.join(__dirname, '../wwwroot/appsettings.local.json');
const hasLocalBeConfig = fs.existsSync(fullPath);
debug('-------------------------------fetching configSettings-------------------------------');
// Add all the variables and remove duplicates
const environmentVariables = [
...new Set(stringifyConfig.match(MATCH_UPPERCASE_UNDERSCORE_VALUES)),
];
// Loop through the app settings per label
const environmentSettingsArray = [];
const labelsFilterArray = Object.values(labelFilterObject);
for (let i = 0; i < labelsFilterArray.length; i++) {
const settings = await getAppConfigSettings(environmentVariables, {
labelFilter: labelsFilterArray[i],
});
environmentSettingsArray.push(...settings);
}
const regexSettings = new RegExp(
// eslint-disable-next-line prefer-template
'\\b(?:' + environmentSettingsArray.map(setting => setting.key).join('|') + ')\\b',
'gi',
);
// Replace all the custom variables with real value
if (environmentSettingsArray.length > 0) {
const configSettings = await JSON.parse(
stringifyConfig.replace(regexSettings, matched => {
const setting = environmentSettingsArray.find(item => item.key === matched);
return setting.value;
}),
);
if (hasLocalBeConfig) {
debug('------------appsettings.local.json found------------');
localBeConfig = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
}
const config = hasLocalBeConfig ? merge(configSettings, localBeConfig) : configSettings;
debug(config);
debug(
'-------------------------------setting configSettings done-------------------------------',
);
callback(config);
}
return true;
};
/**
* Create connection to retrieves the settings from a label or key
*
* @param environmentVariables
* @param labelFilter
* @param keyFilter
* @returns {Promise<*[]|void>}
*/
const getAppConfigSettings = async (environmentVariables, { labelFilter, keyFilter }) => {
try {
const settingsIterator = await client.listConfigurationSettings({
labelFilter, // For example: find every label that starts with uk-dev
keyFilter, // For example: ACCOUNT_URL, MEMBER_URL, OIDC_AC_NODE_LIVE_SECRET etc etc
});
return getTransformedSettingsArray(environmentVariables, settingsIterator);
} catch (error) {
debug(error);
}
return Promise.resolve();
};
/**
* Iterate through the Symbol.asyncIterator and returns an new array with only the data we want
*
* @param environmentVariables
* @param settingsIterator
* @returns {Promise<[]>}
*/
const getTransformedSettingsArray = async (environmentVariables, settingsIterator) => {
const environmentSettingsArray = [];
try {
for await (const setting of settingsIterator) {
const { key, label, value } = setting;
if (environmentVariables.includes(key)) {
environmentSettingsArray.push({
key,
label,
value: await getKeyVaultValue(value),
});
}
}
} catch (error) {
debug(error);
}
return environmentSettingsArray;
};
/**
* Retrieves the value from the Azure keyVault secret
*
* for example:
* '{"uri":"https://uk-shared-slimmingworld.vault.azure.net/secrets/uk-shared-instagram-accesstoken"}'
*
* - parse string into an object
* - check for the vaultName and secretName
* - fetch the keyVaultSecret object
*
* @param appSettingValue
* @returns {Promise<string|*>}
*/
const getKeyVaultValue = async appSettingValue => {
try {
const keyVaultUriObject = JSON.parse(appSettingValue);
if (typeof keyVaultUriObject === 'object') {
const uri = keyVaultUriObject.uri;
const match = uri.match(KEYVAULT_URI_REGEX);
if (match.length > 0) {
const keyVaultUri = JSON.parse(appSettingValue).uri;
const secretUri = keyVaultUri.replace(/https?:\/\//i, '');
const vaultName = secretUri.substring(0, secretUri.indexOf('.'));
const secretName = match[1];
const url = `https://${vaultName}.vault.azure.net`;
const keyVaultClient = new SecretClient(url, credential);
const latestSecretObject = await keyVaultClient.getSecret(secretName);
return latestSecretObject.value;
}
}
} catch (error) {
// debug(error);
}
return appSettingValue;
};
export default getEnvironmentConfig;