From 91df0b6e5d0f6695e26f5aa1f1695ad22089039b Mon Sep 17 00:00:00 2001 From: YannickVR Date: Fri, 4 Oct 2024 09:39:08 +0200 Subject: [PATCH 1/2] fix unit tests --- .../plugin/impl/cdk-build-task-plugin.test.ts | 36 +++++++++--------- .../plugin/impl/sls-build-task-plugin.test.ts | 38 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/test/unit-tests/plugin/impl/cdk-build-task-plugin.test.ts b/test/unit-tests/plugin/impl/cdk-build-task-plugin.test.ts index dfc2e1c7..b4ea859c 100644 --- a/test/unit-tests/plugin/impl/cdk-build-task-plugin.test.ts +++ b/test/unit-tests/plugin/impl/cdk-build-task-plugin.test.ts @@ -48,24 +48,24 @@ describe('when creating cdk plugin', () => { }); -describe('when validating task', () => { - let plugin: CdkBuildTaskPlugin; - let commandArgs: ICdkCommandArgs; - - beforeEach(() => { - plugin = new CdkBuildTaskPlugin(); - commandArgs = plugin.convertToCommandArgs( { - FilePath: './tasks.yaml', - Type: 'cdk', - MaxConcurrentTasks: 1, - FailedTaskTolerance: 4, - LogicalName: 'test-task', - Path: './', - TaskRoleName: 'TaskRole', - OrganizationBinding: { IncludeMasterAccount: true}}, - { organizationFile: './organization.yml'} as any); - }); -}); +// describe('when validating task', () => { +// let plugin: CdkBuildTaskPlugin; +// let commandArgs: ICdkCommandArgs; + +// beforeEach(() => { +// plugin = new CdkBuildTaskPlugin(); +// commandArgs = plugin.convertToCommandArgs( { +// FilePath: './tasks.yaml', +// Type: 'cdk', +// MaxConcurrentTasks: 1, +// FailedTaskTolerance: 4, +// LogicalName: 'test-task', +// Path: './', +// TaskRoleName: 'TaskRole', +// OrganizationBinding: { IncludeMasterAccount: true}}, +// { organizationFile: './organization.yml'} as any); +// }); +// }); describe('when resolving attribute expressions on update', () => { let spawnProcessForAccountSpy: jest.SpyInstance; diff --git a/test/unit-tests/plugin/impl/sls-build-task-plugin.test.ts b/test/unit-tests/plugin/impl/sls-build-task-plugin.test.ts index 75efeca6..9f2372f5 100644 --- a/test/unit-tests/plugin/impl/sls-build-task-plugin.test.ts +++ b/test/unit-tests/plugin/impl/sls-build-task-plugin.test.ts @@ -50,25 +50,25 @@ describe('when creating sls plugin', () => { }); -describe('when validating task', () => { - let plugin: SlsBuildTaskPlugin; - let commandArgs: ISlsCommandArgs; - - beforeEach(() => { - plugin = new SlsBuildTaskPlugin(); - commandArgs = plugin.convertToCommandArgs( { - FilePath: './tasks.yaml', - Type: 'update-serverless.com', - MaxConcurrentTasks: 1, - FailedTaskTolerance: 4, - LogicalName: 'test-task', - Path: './', - Config: './README.md', - TaskRoleName: 'TaskRole', - OrganizationBinding: { IncludeMasterAccount: true}}, - { organizationFile: './organization.yml'} as any); - }); -}); +// describe('when validating task', () => { +// let plugin: SlsBuildTaskPlugin; +// let commandArgs: ISlsCommandArgs; + +// beforeEach(() => { +// plugin = new SlsBuildTaskPlugin(); +// commandArgs = plugin.convertToCommandArgs( { +// FilePath: './tasks.yaml', +// Type: 'update-serverless.com', +// MaxConcurrentTasks: 1, +// FailedTaskTolerance: 4, +// LogicalName: 'test-task', +// Path: './', +// Config: './README.md', +// TaskRoleName: 'TaskRole', +// OrganizationBinding: { IncludeMasterAccount: true}}, +// { organizationFile: './organization.yml'} as any); +// }); +// }); describe('when resolving attribute expressions on update', () => { From 55d449d9a72818afded34463642bab12d8debf9d Mon Sep 17 00:00:00 2001 From: YannickVR Date: Mon, 14 Oct 2024 13:56:41 +0200 Subject: [PATCH 2/2] wip:additional regions --- package.json | 1 + src/aws-provider/aws-organization-reader.ts | 34 +++++++++++++++++++-- src/aws-provider/aws-organization-writer.ts | 1 + src/parser/model/account-resource.ts | 12 +++++++- src/util/aws-util.ts | 24 +++++++++++++++ src/writer/default-template-writer.ts | 10 ++++++ 6 files changed, 79 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b32bc509..a1ce761a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "author": "Olaf Conijn", "license": "MIT", "dependencies": { + "@aws-sdk/client-account": "^3.637.0", "@aws-sdk/client-cloudformation": "^3.637.0", "@aws-sdk/client-ec2": "^3.637.0", "@aws-sdk/client-eventbridge": "^3.637.0", diff --git a/src/aws-provider/aws-organization-reader.ts b/src/aws-provider/aws-organization-reader.ts index 7ce8d627..c6eeb609 100644 --- a/src/aws-provider/aws-organization-reader.ts +++ b/src/aws-provider/aws-organization-reader.ts @@ -2,12 +2,18 @@ import * as IAM from '@aws-sdk/client-iam'; import * as Organizations from '@aws-sdk/client-organizations'; import * as Support from '@aws-sdk/client-support'; import * as STS from '@aws-sdk/client-sts'; +import * as Account from "@aws-sdk/client-account"; import { AwsUtil } from '../util/aws-util'; import { ConsoleUtil } from '../util/console-util'; import { GetOrganizationAccessRoleInTargetAccount, ICrossAccountConfig } from './aws-account-access'; import { performAndRetryIfNeeded } from './util'; export type AWSObjectType = 'Account' | 'OrganizationalUnit' | 'Policy' | string; +export interface OptInRegion { + RegionName?: string; +} + +export type OptInRegions = OptInRegion[]; export type SupportLevel = 'enterprise' | 'business' | 'developer' | 'basic' | string; interface IAWSTags { @@ -31,6 +37,11 @@ interface IAWSAccountWithIAMAttributes { interface IAWSAccountWithSupportLevel { SupportLevel?: SupportLevel; } + +interface IAWSAccountWithOptinRegions { + OptInRegions?: OptInRegions; +} + interface IObjectWithParentId { ParentId: string; } @@ -57,7 +68,7 @@ interface IPolicyTargets { } export type AWSPolicy = Organizations.Policy & IPolicyTargets & IAWSObject; -export type AWSAccount = Organizations.Account & IAWSAccountWithTags & IAWSAccountWithSupportLevel & IAWSAccountWithIAMAttributes & IObjectWithParentId & IObjectWithPolicies & IAWSObject & IAWSObjectWithPartition; +export type AWSAccount = Organizations.Account & IAWSAccountWithTags & IAWSAccountWithSupportLevel & IAWSAccountWithOptinRegions & IAWSAccountWithIAMAttributes & IObjectWithParentId & IObjectWithPolicies & IAWSObject & IAWSObjectWithPartition; export type AWSOrganizationalUnit = Organizations.OrganizationalUnit & IObjectWithParentId & IObjectWithPolicies & IObjectWithAccounts & IAWSObject & IObjectWitOrganizationalUnits & IAWSObjectWithPartition; export type AWSRoot = Organizations.Root & IObjectWithPolicies & IObjectWitOrganizationalUnits; @@ -247,13 +258,15 @@ export class AwsOrganizationReader { let alias: string; let passwordPolicy: IAM.PasswordPolicy; let supportLevel = 'basic'; + let optInRegions: OptInRegions; try { - [tags, alias, passwordPolicy, supportLevel] = await Promise.all([ + [tags, alias, passwordPolicy, supportLevel, optInRegions] = await Promise.all([ AwsOrganizationReader.getTagsForAccount(that, acc.Id), AwsOrganizationReader.getIamAliasForAccount(that, acc.Id), AwsOrganizationReader.getIamPasswordPolicyForAccount(that, acc.Id), AwsOrganizationReader.getSupportLevelForAccount(that, acc.Id), + AwsOrganizationReader.getOptInRegionsForAccount(that, acc.Id), ]); } catch (err) { @@ -276,6 +289,7 @@ export class AwsOrganizationReader { Alias: alias, PasswordPolicy: passwordPolicy, SupportLevel: supportLevel, + OptInRegions: optInRegions, }; const parentOU = organizationalUnits.find(x => x.Id === req.ParentId); @@ -297,6 +311,22 @@ export class AwsOrganizationReader { } } + private static async getOptInRegionsForAccount(that: AwsOrganizationReader, accountId: string): Promise { + try { + await that.organization.getValue(); + const targetRoleConfig = await GetOrganizationAccessRoleInTargetAccount(that.crossAccountConfig, accountId); + const accountClient: Account.AccountClient = AwsUtil.GetAccountService(accountId, targetRoleConfig.role, targetRoleConfig.viaRole, that.isPartition); + + const command = await accountClient.send( new Account.ListRegionsCommand({RegionOptStatusContains: ["ENABLED"],})); + return command.Regions; + } catch (err) { + if (err instanceof Account.AccountServiceException) { + return undefined; + } + throw err; + } + } + private static async getSupportLevelForAccount(that: AwsOrganizationReader, accountId: string): Promise { try { await that.organization.getValue(); diff --git a/src/aws-provider/aws-organization-writer.ts b/src/aws-provider/aws-organization-writer.ts index 362e7f1e..6b34c9e1 100644 --- a/src/aws-provider/aws-organization-writer.ts +++ b/src/aws-provider/aws-organization-writer.ts @@ -1,5 +1,6 @@ import * as Organizations from '@aws-sdk/client-organizations'; import * as IAM from '@aws-sdk/client-iam'; +import * as Account from "@aws-sdk/client-account"; import { CreateCaseCommand } from '@aws-sdk/client-support'; import { STSServiceException } from '@aws-sdk/client-sts'; import { AwsUtil, passwordPolicyEquals } from '../util/aws-util'; diff --git a/src/parser/model/account-resource.ts b/src/parser/model/account-resource.ts index 7eb5f248..e20b17e9 100644 --- a/src/parser/model/account-resource.ts +++ b/src/parser/model/account-resource.ts @@ -20,6 +20,7 @@ export interface IAccountProperties { SupportLevel?: string; OrganizationAccessRoleName?: string; BuildAccessRoleName?: string; + OptInRegions?: any; } export class AccountResource extends Resource { @@ -36,6 +37,7 @@ export class AccountResource extends Resource { public supportLevel?: string; public organizationAccessRoleName?: string; public buildAccessRoleName?: string; + public optInRegions?:any; private props?: IAccountProperties; constructor(root: TemplateRoot, id: string, resource: IResource) { @@ -60,6 +62,7 @@ export class AccountResource extends Resource { this.supportLevel = this.props.SupportLevel; this.organizationAccessRoleName = this.props.OrganizationAccessRoleName; this.buildAccessRoleName = this.props.BuildAccessRoleName; + this.optInRegions = this.props.OptInRegions; if (this.supportLevel !== undefined) { if (!['basic', 'developer', 'business', 'enterprise'].includes(this.supportLevel)) { @@ -67,6 +70,13 @@ export class AccountResource extends Resource { } } + // add this later so existing regions do not get "enabled" + // if (this.optInRegions !== undefined) { + // if (!['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'ap-south-1', ].includes(this.optInRegions)) { + // throw new OrgFormationError(`Unexpected value for SupportLevel on account ${id}. Found: ${this.optInRegions}, Exported one of 'basic', 'developer', 'business', 'enterprise'.`); + // } + // } + if (typeof this.accountId === 'number') { this.accountId = '' + this.accountId; } @@ -79,7 +89,7 @@ export class AccountResource extends Resource { this.organizationAccessRoleName = this.props.OrganizationAccessRoleName; super.throwForUnknownAttributes(resource, id, 'Type', 'Properties'); - super.throwForUnknownAttributes(this.props, id, 'RootEmail', 'AccountName', 'AccountId', 'Alias', 'PartitionAlias', 'PartitionAccountId', 'ServiceControlPolicies', 'Tags', 'PasswordPolicy', 'SupportLevel', 'OrganizationAccessRoleName', 'BuildAccessRoleName'); + super.throwForUnknownAttributes(this.props, id, 'RootEmail', 'AccountName', 'AccountId', 'Alias', 'PartitionAlias', 'PartitionAccountId', 'ServiceControlPolicies', 'Tags', 'PasswordPolicy', 'SupportLevel', 'OrganizationAccessRoleName', 'BuildAccessRoleName', 'OptInRegions'); } public calculateHash(): string { diff --git a/src/util/aws-util.ts b/src/util/aws-util.ts index d60ec3be..79111343 100644 --- a/src/util/aws-util.ts +++ b/src/util/aws-util.ts @@ -3,6 +3,7 @@ import * as ini from 'ini'; import { IAMClient, IAMClientConfig } from '@aws-sdk/client-iam'; import { v4 as uuid } from 'uuid'; import { SupportClient, SupportClientConfig } from '@aws-sdk/client-support'; +import { AccountClient, AccountClientConfig } from "@aws-sdk/client-account"; import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Provider } from '@smithy/types'; import { fromEnv, fromTemporaryCredentials } from '@aws-sdk/credential-providers'; import { DescribeOrganizationCommand, DescribeOrganizationCommandOutput, OrganizationsClient, OrganizationsClientConfig } from '@aws-sdk/client-organizations'; @@ -399,6 +400,28 @@ export class AwsUtil { return this.IamServiceCache[cacheKey]; } + public static GetAccountService(accountId: string, roleInTargetAccount?: string, viaRoleArn?: string, isPartition?: boolean): AccountClient { + AwsUtil.throwIfNowInitiazized(); + const { cacheKey, provider } = AwsUtil.GetCredentialProviderWithRoleAssumptions({ + accountId, + roleInTargetAccount, + viaRoleArn, + isPartition, + }); + const config: AccountClientConfig = { + region: (isPartition) ? this.partitionRegion : AwsUtil.GetDefaultRegion(), + credentials: provider, + defaultsMode: 'standard', + retryMode: 'standard', + maxAttempts: 6, + }; + if (this.AccountServiceCache[cacheKey]) { + return this.AccountServiceCache[cacheKey]; + } + this.AccountServiceCache[cacheKey] = new AccountClient(config); + return this.AccountServiceCache[cacheKey]; + } + /** * Returns an authenticated CloudFormationClient in the provided accountId and region assuming the role provided. * @@ -630,6 +653,7 @@ export class AwsUtil { private static largeTemplateBucketName: string | undefined; private static partitionCredentials: AwsCredentialIdentity | undefined; private static buildProcessAccountId: string; + private static AccountServiceCache: Record = {}; private static IamServiceCache: Record = {}; private static SupportServiceCache: Record = {}; private static OrganizationsServiceCache: Record = {}; diff --git a/src/writer/default-template-writer.ts b/src/writer/default-template-writer.ts index 77ceb47d..1c5dca49 100644 --- a/src/writer/default-template-writer.ts +++ b/src/writer/default-template-writer.ts @@ -343,6 +343,16 @@ export class DefaultTemplateWriter { } } + if (account.OptInRegions) { + const regions = account.OptInRegions; + if (regions.length > 0) { + const regionList = regions + .filter(region => region.RegionName) + .map(region => region.RegionName); + lines.push(new ListLine('OptInRegions', regionList, 6)); + } + } + lines.push(new ListLine('ServiceControlPolicies', policiesList, 6)); lines.push(new EmptyLine());