diff --git a/README.md b/README.md index 4f397322..ccd8d400 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Main Revolver configuration is done in YAML. First line in the config file must |-|-|-| | region | Specifies the default AWS region | - | | timezone | Specifies the default time zone | - | - | timezone_tag | Revolver will read this tag on individual resources to override account-wide timezone | Timezone | + | timezoneTag | Revolver will read this tag on individual resources to override account-wide timezone | Timezone | | organization_role_name | Role to be assumed on the main account from organizations to get the accounts list from it | - | | revolver_role_name | Revolver role name to be assumed on each client account | - | | drivers | List of enabled drivers and their options (see Drivers) | - | @@ -86,7 +86,7 @@ Main Revolver configuration is done in YAML. First line in the config file must defaults: region: ap-southeast-2 timezone: Australia/Melbourne - timezone_tag: Timezone + timezoneTag: Timezone organization_role_name: AWSOrganizationsReadOnly revolver_role_name: ssPowerCycle drivers: @@ -141,7 +141,7 @@ Main Revolver configuration is done in YAML. First line in the config file must plugins: - name: powercycle tagging: strict - availability_tag: Schedule + availabilityTag: Schedule - name: validateTags tag: CostCentre exclude_list: @@ -149,13 +149,13 @@ Main Revolver configuration is done in YAML. First line in the config file must settings: name: helix-dev timezone: Europe/Dublin - timezone_tag: TZ + timezoneTag: TZ plugins: powercycle: active: true configs: - tagging: strict - availability_tag: Schedule + availabilityTag: Schedule validateTags: active: true configs: @@ -205,7 +205,7 @@ Starts AWS resources in the worktime and stops them after hours based on their t |Option|Description|Allowed values|Default| |-|-|-|-| |tagging|Defines tagging format. See below|`strict`|`strict`| -|availability_tag|Name of the tag that contains the schedule|AWS tag name|Schedule| +|availabilityTag|Name of the tag that contains the schedule|AWS tag name|Schedule| When an operation is performed on a resource a tag with a name `ReasonSchedule` (Schedule is replaced with the actual name of the schedule tag) will be set explaining the reason. @@ -219,7 +219,7 @@ plugins: active: true configs: - tagging: strict - availability_tag: Schedule + availabilityTag: Schedule ``` Powercycle plugin supports the following tagging standards: diff --git a/drivers/driverInterface.ts b/drivers/driverInterface.ts index 1cc1dcb4..950de34d 100644 --- a/drivers/driverInterface.ts +++ b/drivers/driverInterface.ts @@ -6,16 +6,16 @@ import { RevolverAction } from '../actions/actions'; export abstract class DriverInterface { protected accountConfig: any; protected driverConfig: any; - protected Id: string; + protected accountId: string; protected logger: Logger; constructor(accountConfig: any, driverConfig: any) { this.accountConfig = accountConfig.settings; this.driverConfig = driverConfig; - this.Id = accountConfig.Id; + this.accountId = accountConfig.accountId; this.logger = logger.getSubLogger( - { name: `${this.accountConfig.name}(${this.Id})` }, - { accountId: this.Id, accountName: this.accountConfig.name, driverName: this.name }, + { name: `${this.accountConfig.name}(${this.accountId})` }, + { accountId: this.accountId, accountName: this.accountConfig.name, driverName: this.name }, ); this.logger.debug(`Initialising driver ${this.name} for account ${this.accountConfig.name}`); } diff --git a/drivers/ebs.ts b/drivers/ebs.ts index ee48311b..a02d17f3 100644 --- a/drivers/ebs.ts +++ b/drivers/ebs.ts @@ -116,10 +116,7 @@ class EBSDriver extends DriverInterface { return ebsVolumes.map( (xe) => - new InstrumentedEBS( - xe, - `arn:aws:ec2:${this.accountConfig.region}:${this.accountConfig.Id}:volume/${xe.VolumeId}`, - ), + new InstrumentedEBS(xe, `arn:aws:ec2:${this.accountConfig.region}:${this.accountId}:volume/${xe.VolumeId}`), ); } } diff --git a/drivers/ec2.ts b/drivers/ec2.ts index ef80ba17..4ae14938 100644 --- a/drivers/ec2.ts +++ b/drivers/ec2.ts @@ -234,10 +234,7 @@ class Ec2Driver extends DriverInterface { return ec2Instances.map( (xi) => - new InstrumentedEc2( - xi, - `arn:aws:ec2:${this.accountConfig.region}:${this.accountConfig.Id}:volume/${xi.InstanceId}`, - ), + new InstrumentedEc2(xi, `arn:aws:ec2:${this.accountConfig.region}:${this.accountId}:volume/${xi.InstanceId}`), ); } } diff --git a/drivers/redshiftCluster.ts b/drivers/redshiftCluster.ts index 3777732a..fd5297c9 100644 --- a/drivers/redshiftCluster.ts +++ b/drivers/redshiftCluster.ts @@ -70,7 +70,7 @@ class RedshiftClusterDriver extends DriverInterface { stopOneCluster(cluster: InstrumentedRedshiftCluster) { let redshift: Redshift; const logger = this.logger; - const tzTagName = this.accountConfig.timezone_tag || 'Timezone'; + const tzTagName = this.accountConfig.timezoneTag || 'Timezone'; const tz = cluster.tag(tzTagName) || this.accountConfig.timezone || 'utc'; const locaTimeNow = dateTime.getTime(tz); const snapshotId = `revolver-cluster-${cluster.resourceId}-${locaTimeNow.toFormat('yyyyLLddHHmmss')}`; @@ -239,7 +239,7 @@ class RedshiftClusterDriver extends DriverInterface { (cluster) => new InstrumentedRedshiftCluster( cluster, - `arn:aws:redshift:${this.accountConfig.region}:${this.Id}:cluster:${cluster.ClusterIdentifier}`, + `arn:aws:redshift:${this.accountConfig.region}:${this.accountId}:cluster:${cluster.ClusterIdentifier}`, ), ), ); diff --git a/drivers/redshiftClusterSnapshot.ts b/drivers/redshiftClusterSnapshot.ts index 00f07cb0..7c72ee82 100644 --- a/drivers/redshiftClusterSnapshot.ts +++ b/drivers/redshiftClusterSnapshot.ts @@ -219,7 +219,7 @@ class RedshiftClusterSnapshotDriver extends DriverInterface { (xs) => new InstrumentedRedshiftClusterSnapshot( xs, - `arn:aws:redshift:${this.accountConfig.region}:${this.Id}:snapshot:${xs.ClusterIdentifier}/${xs.SnapshotIdentifier}`, + `arn:aws:redshift:${this.accountConfig.region}:${this.accountId}:snapshot:${xs.ClusterIdentifier}/${xs.SnapshotIdentifier}`, ), ), ) diff --git a/drivers/snapshot.ts b/drivers/snapshot.ts index cf276c30..164a22fa 100644 --- a/drivers/snapshot.ts +++ b/drivers/snapshot.ts @@ -83,7 +83,7 @@ class SnapshotDriver extends DriverInterface { const ec2 = await new EC2({ credentials: creds, region: this.accountConfig.region }); const snapshots = await paginateAwsCall(ec2.describeSnapshots.bind(ec2), 'Snapshots', { - OwnerIds: [this.Id], + OwnerIds: [this.accountId], }); logger.debug('Snapshots %d found', snapshots.length); diff --git a/lib/accountRevolver.ts b/lib/accountRevolver.ts index cc60996a..0076b26b 100644 --- a/lib/accountRevolver.ts +++ b/lib/accountRevolver.ts @@ -27,12 +27,12 @@ export class AccountRevolver { this.config = accountConfig; this.logger = logger.getSubLogger( { name: 'accountRevolver' }, - { accountId: this.config.settings.Id, accountName: this.config.settings.name }, + { accountId: this.config.accountId, accountName: this.config.settings.name }, ); } async initialise(): Promise { - this.logger.info('Initialising revolver'); + this.logger.info(`Initialising revolver for account ${this.config.settings.name}(${this.config.accountId})`); const activePlugins = Object.keys(this.config.plugins) .filter((xp) => this.supportedPlugins.indexOf(xp) > -1) diff --git a/lib/config.ts b/lib/config.ts index a7ff1c4a..c816844e 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -3,61 +3,28 @@ import { promises as fs } from 'fs'; import path = require('node:path'); import yaml from 'js-yaml'; import { Organizations, S3 } from 'aws-sdk'; -import { paginateAwsCall, uniqueBy } from './common'; -import { deepmerge } from 'deepmerge-ts'; - -class Settings { - private settings: { [key: string]: any }; - - constructor() { - this.settings = {}; - } - - store(settings: { [key: string]: any }) { - this.settings = settings; - } - - get(key: string) { - return this.settings[key]; - } -} - -export const settings = new Settings(); +import { paginateAwsCall } from './common'; +import merge from 'ts-deepmerge'; export class RevolverConfig { validateConfig(data: string) { const config: any = yaml.load(data); - logger.debug('Read Revolver config: %j', config); - if (!Array.isArray(config.accounts.include_list)) { - throw new Error("Invalid configuration. 'include_list' key is either missing or not an array"); + if (!Array.isArray(config.accounts.includeList)) { + throw new Error('Invalid configuration: "includeList" key is either missing or not an array'); } - if (!Array.isArray(config.accounts.exclude_list)) { - throw new Error("Invalid configuration. 'exclude_list' key is either missing or not an array"); + if (!Array.isArray(config.accounts.excludeList)) { + throw new Error('Invalid configuration: "excludeList" key is either missing or not an array'); } - settings.store(config.settings); // merge default settings and extract some info - config.organizations = config.organizations.map((r: any) => deepmerge({}, config.defaults, r)); - config.accounts.include_list = config.accounts.include_list.map((r: any) => deepmerge(config.defaults, r)); - config.accounts.exclude_list = config.accounts.exclude_list.map((r: any) => deepmerge(config.defaults, r)); - - config.defaults.settings.organizationRoleName = config.defaults.settings.organization_role_name; - config.defaults.settings.revolverRoleName = config.defaults.settings.revolver_role_name; - - config.organizations.map((org: any) => { - org.Id = org.account_id; - org.settings.organizationRoleName = org.settings.organization_role_name; - org.settings.revolverRoleName = org.settings.revolver_role_name; + config.organizations.forEach((org: any) => { + org.settings = Object.assign({}, config.defaults.settings, org.settings); }); - config.accounts.include_list.map((acc: any) => { - acc.Id = acc.account_id; - acc.settings.revolverRoleName = acc.settings.revolver_role_name; - }); - config.accounts.exclude_list.map((acc: any) => { - acc.Id = acc.account_id; - acc.settings.revolverRoleName = acc.settings.revolver_role_name; + + config.accounts.includeList.forEach((account: any) => { + account.settings = Object.assign({}, config.defaults.settings, account.settings); }); - logger.debug('Final Revolver config: %j', config); + logger.debug('Read Revolver config: %j', config); return config; } @@ -83,11 +50,14 @@ export class RevolverConfig { const client = new Organizations({ credentials: cr, region: orgsRegion }); const accounts = await paginateAwsCall(client.listAccounts.bind(client), 'Accounts'); accounts.forEach((account) => { + account.accountId = account.Id; + delete account.Id; account.settings = { name: account.Name, region: cr.settings.region, timezone: cr.settings.timezone, - revolverRoleName: cr.settings.revolver_role_name, + timezoneTag: cr.settings.timezoneTag, + revolverRoleName: cr.settings.revolverRoleName, }; }); return accounts; @@ -99,22 +69,28 @@ export class RevolverConfig { } filterAccountsList(orgsAccountsList: any[], config: any) { - logger.info('%d Accounts found on the Organizations listed', orgsAccountsList.length); - logger.info('Getting accounts from include/exclude lists..'); - logger.info('%d accounts found on include_list', config.accounts.include_list.length); - logger.info('%d accounts found on exclude_list', config.accounts.exclude_list.length); - const filteredAccountsList = config.accounts.include_list - // concat include_list - .concat(orgsAccountsList) - // delete exclude_list - .filter((xa: any) => !config.accounts.exclude_list.find((xi: any) => xi.Id === xa.Id)) - // build assumeRoleArn string, extract account_id and revolver_role_name - .map((account: any) => { - account.settings.assumeRoleArn = `arn:aws:iam::${account.Id}:role/${account.settings.revolverRoleName}`; - return account; - }); + logger.info(`${orgsAccountsList.length} Accounts found on the Organizations listed`); + logger.info(`${config.accounts.includeList.length} accounts found on include_list`); + logger.info(`${config.accounts.excludeList.length} accounts found on exclude_list`); + // exclude specified in includeList accounts from the org list + const orgWithoutIncludeList = orgsAccountsList.filter( + (xa: any) => + !config.accounts.includeList.find( + (xi: any) => xi.accountId === xa.accountId && xi.settings.region === xa.settings.region, + ), + ); + const accountList = orgWithoutIncludeList.concat(config.accounts.includeList); + // exclude accounts specified in excludeList + const filteredAccountsList = accountList.filter( + (xa: any) => !config.accounts.excludeList.find((xi: any) => xi.accountId === xa.accountId), + ); + // build assumeRoleArn string, extract account_id and revolver_role_name + const updatedAccountsList = filteredAccountsList.map((xa: any) => { + const account: any = merge.withOptions({ mergeArrays: false }, xa, config.defaults); + account.settings.assumeRoleArn = `arn:aws:iam::${account.accountId}:role/${account.settings.revolverRoleName}`; + return account; + }); - // remove duplicated accounts - return uniqueBy(filteredAccountsList, (account: any) => JSON.stringify([account.Id, account.settings.region])); + return updatedAccountsList; } } diff --git a/package-lock.json b/package-lock.json index bdb9386b..1b40544d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "aws-sdk": "^2.1472.0", - "deepmerge-ts": "^5.1.0", "js-yaml": "^4.1.0", "luxon": "^3.4.4", "proxy-agent": "^6.3.1", + "ts-deepmerge": "^6.2.0", "tslog": "^4.9.2" }, "devDependencies": { @@ -710,9 +710,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.4.tgz", - "integrity": "sha512-6I0fMH8Aoy2lOejL3s4LhyIYX34DPwY8bl5xlNjBvUEk8OHrcuzsFt+Ied4LvJihbtXPM+8zUqdydfIti86v9g==", + "version": "20.11.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", + "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1564,14 +1564,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge-ts": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", - "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -3929,9 +3921,9 @@ } }, "node_modules/prettier": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.2.tgz", - "integrity": "sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.3.tgz", + "integrity": "sha512-QNhUTBq+mqt1oH1dTfY3phOKNhcDdJkfttHI6u0kj7M2+c+7fmNKlgh2GhnHiqMcbxJ+a0j2igz/2jfl9QKLuw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -4681,6 +4673,14 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-deepmerge": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.0.tgz", + "integrity": "sha512-2qxI/FZVDPbzh63GwWIZYE7daWKtwXZYuyc8YNq0iTmMUwn4mL0jRLsp6hfFlgbdRSR4x2ppe+E86FnvEpN7Nw==", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index ca0fb163..aa402ff4 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,10 @@ "license": "MIT", "dependencies": { "aws-sdk": "^2.1472.0", - "deepmerge-ts": "^5.1.0", "js-yaml": "^4.1.0", "luxon": "^3.4.4", "proxy-agent": "^6.3.1", + "ts-deepmerge": "^6.2.0", "tslog": "^4.9.2" }, "devDependencies": { diff --git a/plugins/pluginInterface.ts b/plugins/pluginInterface.ts index df9fca4f..3f732f26 100644 --- a/plugins/pluginInterface.ts +++ b/plugins/pluginInterface.ts @@ -10,12 +10,12 @@ export abstract class RevolverPlugin { constructor(accountConfig: any, pluginName: string, pluginConfig: any) { this.accountConfig = accountConfig.settings; - this.accountId = accountConfig.Id; + this.accountId = accountConfig.accountId; this.pluginConfig = pluginConfig; this.pluginConfig.name = pluginName; this.logger = logger.getSubLogger( { name: this.accountConfig.name }, - { accountId: this.accountConfig.Id, accountName: this.accountConfig.name, pluginName }, + { accountId: this.accountId, accountName: this.accountConfig.name, pluginName }, ); this.logger.debug(`Initialising plugin ${this.name} for account ${this.accountConfig.name}`); } diff --git a/plugins/powercycle.ts b/plugins/powercycle.ts index 73fac8cf..065c2592 100644 --- a/plugins/powercycle.ts +++ b/plugins/powercycle.ts @@ -13,8 +13,8 @@ export default class PowerCyclePlugin extends RevolverPlugin { constructor(accountConfig: any, pluginName: string, pluginConfig: any) { super(accountConfig, pluginName, pluginConfig); - this.scheduleTagName = this.pluginConfig.availability_tag || 'Schedule'; - this.timezoneTagName = this.accountConfig.timezone_tag || 'Timezone'; + this.scheduleTagName = this.pluginConfig.availabilityTag || 'Schedule'; + this.timezoneTagName = this.accountConfig.timezoneTag || 'Timezone'; this.warningTagName = `Warning${this.scheduleTagName}`; this.reasonTagName = `Reason${this.scheduleTagName}`; } diff --git a/revolver-config-example.yaml b/revolver-config-example.yaml index 448b39c6..ffde27ba 100644 --- a/revolver-config-example.yaml +++ b/revolver-config-example.yaml @@ -3,9 +3,9 @@ defaults: settings: region: ap-southeast-2 timezone: Australia/Melbourne - timezone_tag: Timezone - organization_role_name: AWSOrganizationsReadOnly - revolver_role_name: ssPowerCycle + timezoneTag: Timezone + organizationRoleName: AWSOrganizationsReadOnly + revolverRoleName: ssPowerCycle drivers: - name: ec2 @@ -29,7 +29,7 @@ defaults: active: true configs: - tagging: strict - availability_tag: Schedule + availabilityTag: Schedule validateTags: active: true configs: @@ -45,9 +45,8 @@ defaults: tag: Name tagMissing: - warn - excludeResourceTypes: - - ebs - - snapshot + onlyResourceTypes: + - ec2 tagNotMatch: [] - tag: Schedule @@ -59,12 +58,22 @@ defaults: - rdsCluster tagNotMatch: [] -organizations: [] +organizations: + - accountId: 000000000000 + settings: + region: ap-southeast-2 + name: eh-global-apse2 + - accountId: 111111111111 + settings: + region: eu-west-1 + name: eh-global-euw1 accounts: - include_list: - - account_id: "000000000000" + includeList: + - accountId: "222222222222" settings: name: whatdev - - exclude_list: [] + excludeList: + - accountId: "333333333333" + settings: + name: whatprod diff --git a/revolver.ts b/revolver.ts index e6d1e002..306e4584 100644 --- a/revolver.ts +++ b/revolver.ts @@ -44,10 +44,12 @@ export const handler: ScheduledHandler = async (event: EventBridgeEvent<'Schedul const organisationCreds = await Promise.all( config.organizations.flatMap((xa: any) => { logger.info('Getting list of accounts from %s organization..', xa.settings.name); - return assume.connectTo(`arn:aws:iam::${xa.Id}:role/${xa.settings.organizationRoleName}`).then((cred: any) => { - cred.settings = xa.settings; - return cred; - }); + return assume + .connectTo(`arn:aws:iam::${xa.accountId}:role/${xa.settings.organizationRoleName}`) + .then((cred: any) => { + cred.settings = xa.settings; + return cred; + }); }), ); const orgsAccountsList = await configMethods.getOrganisationsAccounts(organisationCreds);