diff --git a/src/commands/engage/install/agents.ts b/src/commands/engage/install/agents.ts index 4f4932f0..fcca3b25 100644 --- a/src/commands/engage/install/agents.ts +++ b/src/commands/engage/install/agents.ts @@ -5,7 +5,7 @@ import { installAgents } from '../../../lib/engage/services/install-service.js'; import { highlight } from '../../../lib/logger.js'; export class EngageInstallAgentsCommand extends Command { - static override summary = 'Amplify API Gateway / Apigee X Gateway / Amazon API Gateway / Azure API Gateway / Azure EventHub / Backstage / GitLab / Istio / Kafka /' + static override summary = 'Amplify API Gateway / Apigee X Gateway / AWS / Azure API Gateway / Azure EventHub / Backstage / GitLab / Istio / Kafka /' + ' Graylog / IBM API Connect / SwaggerHub / Software AG WebMethods / Traceable / SAP API Portal / Sensedia / WSO2'; static override aliases = [ 'central:install:agents' ]; diff --git a/src/lib/engage/services/create-service.ts b/src/lib/engage/services/create-service.ts index a37474b1..c6380a08 100644 --- a/src/lib/engage/services/create-service.ts +++ b/src/lib/engage/services/create-service.ts @@ -1,7 +1,7 @@ import chalk from 'chalk'; import { ApiServerClient } from '../clients-external/apiserverclient.js'; import { DefinitionsManager } from '../results/DefinitionsManager.js'; -import { AgentResourceCreateResult, AgentResourceKind, AgentTypes, ApiServerClientSingleResult, ApiServerVersions, BundleType, CreateCommandParams, CreateCommandResult, CreateEnvironmentCommandParams, DataPlaneNames, EngageCommandParams, GenericResource } from '../types.js'; +import { AgentResourceCreateResult, AgentResourceKind, AgentTypes, ApiServerClientSingleResult, BundleType, CreateCommandParams, CreateCommandResult, CreateEnvironmentCommandParams, DataPlaneNames, EngageCommandParams, GenericResource } from '../types.js'; import { getLatestServedAPIVersion, loadAndVerifySpecs, verifyFile } from '../utils/utils.js'; import { askInput, askList } from '../utils/basic-prompts.js'; import { askAgentName, askEnvironmentName } from '../utils/agents/inputs.js'; diff --git a/src/lib/engage/types.ts b/src/lib/engage/types.ts index eaeb916f..b0263b47 100644 --- a/src/lib/engage/types.ts +++ b/src/lib/engage/types.ts @@ -752,6 +752,25 @@ export enum GatewayMode { GatewayOnlyMode = 'Gateway only', } +export enum AWSGatewayMode { + APIGateway = 'api-gateway', + AgentCoreGateway = 'agent-core-gateway', +} + +export class AWSAgentCoreConfig { + logGroupPrefix?: string; + iamAuthEnabled?: boolean; + cloudTrailEnabled?: boolean; + cloudTrailBucket?: string; + + constructor(logGroupPrefix?: string, iamAuthEnabled?: boolean, cloudTrailEnabled?: boolean, cloudTrailBucket?: string) { + this.logGroupPrefix = logGroupPrefix ?? ''; + this.iamAuthEnabled = iamAuthEnabled ?? false; + this.cloudTrailEnabled = cloudTrailEnabled ?? false; + this.cloudTrailBucket = cloudTrailBucket ?? ''; + } +} + export enum AgentNames { AKAMAI_CA = 'akamai-compliance-agent', AWS_DA = 'aws-apigw-discovery-agent', @@ -787,7 +806,7 @@ export enum GatewayTypes { AKAMAI = 'Akamai', EDGE_GATEWAY = 'Amplify API Gateway', APIGEEX_GATEWAY = 'Apigee X Gateway', - AWS_GATEWAY = 'Amazon API Gateway', + AWS_GATEWAY = 'AWS', AZURE_GATEWAY = 'Azure API Gateway', AZURE_EVENTHUB = 'Azure EventHub', GITLAB = 'GitLab', @@ -806,7 +825,7 @@ export enum GatewayTypes { export enum SaaSGatewayTypes { AKAMAI = 'Akamai', - AWS_GATEWAY = 'Amazon API Gateway', + AWS_GATEWAY = 'AWS', APIGEEX_GATEWAY = 'Apigee X Gateway', GITHUB = 'GitHub', SWAGGERHUB = 'SwaggerHub', diff --git a/src/lib/engage/utils/agents/flows/awsAgents.ts b/src/lib/engage/utils/agents/flows/awsAgents.ts index e97ea2fa..b4e39353 100644 --- a/src/lib/engage/utils/agents/flows/awsAgents.ts +++ b/src/lib/engage/utils/agents/flows/awsAgents.ts @@ -67,7 +67,7 @@ export const AWSPrompts = { PUBLIC_IP: 'Assign a Public IP Address to this, only change if your VPC has a NAT Gateway', SECURITY_GROUP: 'Enter the Security Group for the EC2 Instance of ECS Container', SETUP_APIGW_CW: - 'The Amazon API Gateway service requires a role to write usage logs to Cloud Watch. Do you want to configure that?', + 'The AWS API Gateway service requires a role to write usage logs to Cloud Watch. Do you want to configure that?', SSH_LOCATION: 'Enter the IP address range that can be used to SSH to the EC2 instances', SSM_PRIVATE: 'Enter the name of the SSM Parameter holding the Private Key', SSM_PUBLIC: 'Enter the name of the SSM Parameter holding the Public Key', @@ -77,6 +77,14 @@ export const AWSPrompts = { FULL_TRANSACTION_LOGGING: 'Do you want to enable Full Transaction Logging? Please note that CloudWatch costs would increase when Full Transaction Logging is enabled', TA_QUEUE: 'Enter the traceability queue name', VPC_ID: 'Enter the VPC ID to deploy the EC2 instance to. Leave blank to create entire infrastructure', + AGENT_CORE_GATEWAY_MODE: 'Do you want to enable AgentCore Gateway Mode? (If not, the default will be to run the agent in API Gateway mode)', + AGENT_CORE_LOG_GROUP_PREFIX: 'Enter the prefix for the AgentCore Gateway vendored logs', + AGENT_CORE_IAM_AUTH: 'Do you want to enable IAM Authentication for AgentCore Gateway requests?', + ENTER_MORE_COGNITO_USER_POOL_IDS: 'Do you want to enter another Cognito User Pool ID for AgentCore Gateway mode?', + COGNITO: 'Enter the List of AWS Cognito user pool IDs used for authentication in AgentCore Gateway mode', + COGNITO_USER_POOL_ID: 'Enter the User Pool ID for the Cognito User Pool the AgentCore will use for authentication', + AGENTCORE_CLOUDTRAILENABLED: 'Do you want to enable CloudTrail-based consumer attribution for Cognito gateway?', + AGENTCORE_CLOUDTRAILBUCKET: 'Enter the name of the S3 bucket that stores the CloudTrail data-event logs' }; export const askBundleType = async (): Promise => { @@ -241,7 +249,7 @@ async function configureECSDeployment(awsAgentValues: helpers.AWSAgentValues): P } export const gatewayConnectivity = async (installConfig: AgentInstallConfig): Promise => { - installConfig.log('\nCONNECTION TO AMAZON API GATEWAY:'); + installConfig.log('\nCONNECTION TO AWS:'); installConfig.log( chalk.gray( 'You need credentials for executing the AWS CLI commands.\n' @@ -276,69 +284,111 @@ export const gatewayConnectivity = async (installConfig: AgentInstallConfig): Pr // AWS Region awsAgentValues.region = await helpers.askAWSRegion(); - // S3 bucket - awsAgentValues.cloudFormationConfig.AgentResourcesBucket = (await askInput({ - msg: AWSPrompts.S3_BUCKET, - validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP, InvalidMsg.S3_BUCKET), - })) as string; - - // APIGWCWRoleSetup - awsAgentValues.cloudFormationConfig.APIGWCWRoleSetup = await askToCreateRoleSetup(); - - // APIGWTrafficLogGroupName - const apiGWTrafficLogGroupName = (await askInput({ - msg: AWSPrompts.APIGW_LOG_GROUP, - defaultValue: awsAgentValues.cloudFormationConfig.APIGWTrafficLogGroupName, - validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), - })) as string; - awsAgentValues.logGroup = apiGWTrafficLogGroupName; - awsAgentValues.cloudFormationConfig.APIGWTrafficLogGroupName = apiGWTrafficLogGroupName; - - // StageTagName - const stageTagName = (await askInput({ - msg: AWSPrompts.STAGE_TAG_NAME, - validate: validateInputLength(STAGE_TAG_NAME_LENGTH, 'Maximum length of \'stage tag name\' is 127'), - })) as string; - awsAgentValues.stageTagName = stageTagName; - - // FullTransactionLogging - const fullTransactionLogging = ((await askList({ - msg: AWSPrompts.FULL_TRANSACTION_LOGGING, - choices: YesNoChoices, + // Determine gateway mode early to skip irrelevant API GW prompts + awsAgentValues.agentCoreGatewayMode = (await askList({ + msg: AWSPrompts.AGENT_CORE_GATEWAY_MODE, default: YesNo.No, - })) === YesNo.Yes); - - awsAgentValues.fullTransactionLogging = fullTransactionLogging; - - // set agent versions - awsAgentValues.cloudFormationConfig.DiscoveryAgentVersion = installConfig.daVersion; - awsAgentValues.cloudFormationConfig.TraceabilityAgentVersion = installConfig.taVersion; + choices: YesNoChoices, + })) === YesNo.Yes; + + if (awsAgentValues.agentCoreGatewayMode) { + awsAgentValues.agentCore.iamAuthEnabled = (await askList({ + msg: AWSPrompts.AGENT_CORE_IAM_AUTH, + default: YesNo.No, + choices: YesNoChoices, + })) === YesNo.Yes; + installConfig.log(chalk.gray(AWSPrompts.COGNITO)); + const cognitoUserPoolIDs: string[] = []; + let askCognitoUserPools = true; + + while (askCognitoUserPools) { + const userPoolId = (await askInput({ + msg: AWSPrompts.COGNITO_USER_POOL_ID, + })) as string; + + cognitoUserPoolIDs.push(userPoolId); + + askCognitoUserPools = await askList({ + msg: AWSPrompts.ENTER_MORE_COGNITO_USER_POOL_IDS, + choices: YesNoChoices, + default: YesNo.No, + }) === YesNo.Yes; + } - // Configure appropriate Gateway type - switch (awsAgentValues.cloudFormationConfig.DeploymentType) { - case DeploymentTypes.ECS_FARGATE: { - awsAgentValues = await configureECSDeployment(awsAgentValues); - break; + awsAgentValues.cognitoUserPoolIDs = cognitoUserPoolIDs; + if (installConfig.switches.isTaEnabled) { + awsAgentValues.agentCore.logGroupPrefix = (await askInput({ + msg: AWSPrompts.AGENT_CORE_LOG_GROUP_PREFIX, + defaultValue: awsAgentValues.agentCore.logGroupPrefix !== '' ? awsAgentValues.agentCore.logGroupPrefix : undefined, + allowEmptyInput: true, + })) as string; + + awsAgentValues.agentCore.cloudTrailEnabled = (await askList({ + msg: AWSPrompts.AGENTCORE_CLOUDTRAILENABLED, + default: YesNo.No, + choices: YesNoChoices, + })) === YesNo.Yes; + + awsAgentValues.agentCore.cloudTrailBucket = (await askInput({ + msg: AWSPrompts.AGENTCORE_CLOUDTRAILBUCKET, + defaultValue: awsAgentValues.agentCore.cloudTrailBucket !== '' ? awsAgentValues.agentCore.cloudTrailBucket : undefined, + allowEmptyInput: false, + })) as string; } - case DeploymentTypes.EC2: { - awsAgentValues = await configureEC2Deployment(awsAgentValues, installConfig.log); - break; + } else { + // API Gateway mode — collect all API GW-specific configuration + awsAgentValues.cloudFormationConfig.AgentResourcesBucket = (await askInput({ + msg: AWSPrompts.S3_BUCKET, + validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP, InvalidMsg.S3_BUCKET), + })) as string; + + awsAgentValues.cloudFormationConfig.APIGWCWRoleSetup = await askToCreateRoleSetup(); + + const apiGWTrafficLogGroupName = (await askInput({ + msg: AWSPrompts.APIGW_LOG_GROUP, + defaultValue: awsAgentValues.cloudFormationConfig.APIGWTrafficLogGroupName, + validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), + })) as string; + awsAgentValues.logGroup = apiGWTrafficLogGroupName; + awsAgentValues.cloudFormationConfig.APIGWTrafficLogGroupName = apiGWTrafficLogGroupName; + + awsAgentValues.stageTagName = (await askInput({ + msg: AWSPrompts.STAGE_TAG_NAME, + validate: validateInputLength(STAGE_TAG_NAME_LENGTH, 'Maximum length of \'stage tag name\' is 127'), + })) as string; + + awsAgentValues.fullTransactionLogging = ((await askList({ + msg: AWSPrompts.FULL_TRANSACTION_LOGGING, + choices: YesNoChoices, + default: YesNo.No, + })) === YesNo.Yes); + + awsAgentValues.cloudFormationConfig.DiscoveryAgentVersion = installConfig.daVersion; + awsAgentValues.cloudFormationConfig.TraceabilityAgentVersion = installConfig.taVersion; + + switch (awsAgentValues.cloudFormationConfig.DeploymentType) { + case DeploymentTypes.ECS_FARGATE: { + awsAgentValues = await configureECSDeployment(awsAgentValues); + break; + } + case DeploymentTypes.EC2: { + awsAgentValues = await configureEC2Deployment(awsAgentValues, installConfig.log); + break; + } } - } - - // DiscoveryAgentLogGroupName - awsAgentValues.cloudFormationConfig.DiscoveryAgentLogGroupName = (await askInput({ - msg: AWSPrompts.DA_LOG_GROUP, - defaultValue: awsAgentValues.cloudFormationConfig.DiscoveryAgentLogGroupName, - validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), - })) as string; - // TraceabilityAgentLogGroupName - awsAgentValues.cloudFormationConfig.TraceabilityAgentLogGroupName = (await askInput({ - msg: AWSPrompts.TA_LOG_GROUP, - defaultValue: awsAgentValues.cloudFormationConfig.TraceabilityAgentLogGroupName, - validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), - })) as string; + awsAgentValues.cloudFormationConfig.DiscoveryAgentLogGroupName = (await askInput({ + msg: AWSPrompts.DA_LOG_GROUP, + defaultValue: awsAgentValues.cloudFormationConfig.DiscoveryAgentLogGroupName, + validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), + })) as string; + + awsAgentValues.cloudFormationConfig.TraceabilityAgentLogGroupName = (await askInput({ + msg: AWSPrompts.TA_LOG_GROUP, + defaultValue: awsAgentValues.cloudFormationConfig.TraceabilityAgentLogGroupName, + validate: validateRegex(helpers.AWSRegexPatterns.AWS_REGEXP_LOG_GROUP_NAME, InvalidMsg.LOG_GROUP), + })) as string; + } return awsAgentValues; }; diff --git a/src/lib/engage/utils/agents/flows/awsSaasAgents.ts b/src/lib/engage/utils/agents/flows/awsSaasAgents.ts index f8997e82..d0bd1440 100644 --- a/src/lib/engage/utils/agents/flows/awsSaasAgents.ts +++ b/src/lib/engage/utils/agents/flows/awsSaasAgents.ts @@ -2,7 +2,7 @@ import chalk from 'chalk'; import logger from '../../../../logger.js'; import { ApiServerClient } from '../../../clients-external/apiserverclient.js'; import { DefinitionsManager } from '../../../results/DefinitionsManager.js'; -import { AgentConfigTypes, AgentInstallConfig, AgentNames, AgentTypes, BundleType, GatewayTypes, InstallationFlowMethods, SaaSGatewayTypes, YesNo, YesNoChoices } from '../../../types.js'; +import { AgentConfigTypes, AgentInstallConfig, AgentNames, AgentTypes, AWSAgentCoreConfig, AWSGatewayMode, BundleType, GatewayTypes, InstallationFlowMethods, SaaSGatewayTypes, YesNo, YesNoChoices } from '../../../types.js'; import { askInput, askList, validateInputLength, validateRegex } from '../../basic-prompts.js'; import * as helpers from '../index.js'; import { @@ -23,12 +23,17 @@ class AWSDataplaneConfig extends DataplaneConfig { accessLogARN: string; fullTransactionLogging: boolean; stageTagName: string; + gatewayMode: AWSGatewayMode; + agentCore: AWSAgentCoreConfig; + cognitoUserPoolIDs: string[]; - constructor(arn: string, enableFullTransactionLogging: boolean, stageTagName: string) { + constructor(arn: string, enableFullTransactionLogging: boolean, stageTagName: string, agentCoreConfig: AWSAgentCoreConfig, cognitoUserPoolIDs: string[]) { super('AWS'); this.accessLogARN = arn; this.fullTransactionLogging = enableFullTransactionLogging; this.stageTagName = stageTagName; + this.agentCore = agentCoreConfig; + this.cognitoUserPoolIDs = cognitoUserPoolIDs; } } @@ -47,6 +52,9 @@ class SaasAWSAgentValues extends SaasAgentValues { accessLogARN: string; fullTransactionLogging: boolean; stageTagName: string; + agentCoreGatewayMode: boolean; + agentCore: AWSAgentCoreConfig; + cognitoUserPoolIDs: string[]; constructor() { super(); @@ -59,8 +67,9 @@ class SaasAWSAgentValues extends SaasAgentValues { this.accessLogARN = ''; this.fullTransactionLogging = false; this.stageTagName = ''; + this.agentCore = {} as AWSAgentCoreConfig; + this.cognitoUserPoolIDs = [] as string[]; } - override getAccessData(): string { if (this.authType === AWSAuthType.KEYS) { return JSON.stringify({ @@ -87,6 +96,14 @@ const SaasPrompts = { ACCESS_LOG_ARN: 'Enter the ARN for the Access Log that the Discovery will add and the Traceability will use', STAGE_TAG_NAME: 'Enter the name of the tag on AWS API Gateway Stage that holds mapped stage on Amplify Engage', FULL_TRANSACTION_LOGGING: 'Do you want to enable Full Transaction Logging? Please note that CloudWatch costs would increase when Full Transaction Logging is enabled', + AGENT_CORE_GATEWAY_MODE: 'Do you want to enable AgentCore Gateway Mode? (If not, the default will be to run the agent in API Gateway mode)', + AGENT_CORE_LOG_GROUP_PREFIX: 'Enter the prefix for the AgentCore Gateway vendored logs', + AGENT_CORE_IAM_AUTH: 'Do you want to enable IAM Authentication for AgentCore Gateway requests?', + ENTER_MORE_COGNITO_USER_POOL_IDS: 'Do you want to enter another Cognito User Pool ID for AgentCore Gateway mode?', + COGNITO: 'Enter the List of AWS Cognito user pool IDs used for authentication in AgentCore Gateway mode', + COGNITO_USER_POOL_ID: 'Enter the User Pool ID for the Cognito User Pool the AgentCore will use for authentication', + AGENTCORE_CLOUDTRAILENABLED: 'Do you want to enable CloudTrail-based consumer attribution for Cognito gateway?', + AGENTCORE_CLOUDTRAILBUCKET: 'Enter the name of the S3 bucket that stores the CloudTrail data-event logs' }; export const askBundleType = async (): Promise => { @@ -155,11 +172,69 @@ const askForAWSCredentials = async (agentValues: SaasAWSAgentValues, log: (text: return agentValues; }; +const askForAgentCoreGatewayMode = async (agentValues: SaasAWSAgentValues, installConfig: AgentInstallConfig): Promise => { + agentValues.agentCoreGatewayMode = (await askList({ + msg: SaasPrompts.AGENT_CORE_GATEWAY_MODE, + default: YesNo.No, + choices: YesNoChoices, + })) === YesNo.Yes; + + if (agentValues.agentCoreGatewayMode) { + agentValues.agentCore.iamAuthEnabled = (await askList({ + msg: SaasPrompts.AGENT_CORE_IAM_AUTH, + default: YesNo.No, + choices: YesNoChoices, + })) === YesNo.Yes; + installConfig.log(chalk.gray(SaasPrompts.COGNITO)); + const cognitoUserPoolIDs: string[] = []; + let askCognitoUserPools = true; + + while (askCognitoUserPools) { + const userPoolId = (await askInput({ + msg: SaasPrompts.COGNITO_USER_POOL_ID, + })) as string; + + cognitoUserPoolIDs.push(userPoolId); + + askCognitoUserPools = await askList({ + msg: SaasPrompts.ENTER_MORE_COGNITO_USER_POOL_IDS, + choices: YesNoChoices, + default: YesNo.No, + }) === YesNo.Yes; + } + + agentValues.cognitoUserPoolIDs = cognitoUserPoolIDs; + + if (installConfig.switches.isTaEnabled) { + agentValues.agentCore.logGroupPrefix = (await askInput({ + msg: SaasPrompts.AGENT_CORE_LOG_GROUP_PREFIX, + defaultValue: agentValues.agentCore.logGroupPrefix !== '' ? agentValues.agentCore.logGroupPrefix : undefined, + allowEmptyInput: true, + })) as string; + + agentValues.agentCore.cloudTrailEnabled = (await askList({ + msg: SaasPrompts.AGENTCORE_CLOUDTRAILENABLED, + default: YesNo.No, + choices: YesNoChoices, + })) === YesNo.Yes; + + agentValues.agentCore.cloudTrailBucket = (await askInput({ + msg: SaasPrompts.AGENTCORE_CLOUDTRAILBUCKET, + defaultValue: agentValues.agentCore.cloudTrailBucket !== '' ? agentValues.agentCore.cloudTrailBucket : undefined, + allowEmptyInput: false, + })) as string; + } + + } + + return agentValues; +}; + export const gatewayConnectivity = async (installConfig: AgentInstallConfig): Promise => { - installConfig.log('\nCONNECTION TO AMAZON API GATEWAY:'); + installConfig.log('\nCONNECTION TO AWS:'); installConfig.log( chalk.gray( - 'The Discovery Agent needs to connect to the AWS API Gateway to discover API\'s for publishing to Amplify Engage' + 'The Discovery Agent needs to connect to the AWS to discover API\'s for publishing to Amplify Engage' ) ); @@ -174,6 +249,8 @@ export const gatewayConnectivity = async (installConfig: AgentInstallConfig): Pr validate: validateInputLength(STAGE_TAG_NAME_LENGTH, 'Maximum length of \'stage tag name\' is 127'), })) as string; + agentValues = await askForAgentCoreGatewayMode(awsValues, installConfig); + if (installConfig.switches.isTaEnabled) { installConfig.log(chalk.gray('\nThe access log ARN is a cloud watch log group amazon resource name')); awsValues.accessLogARN = (await askInput({ @@ -226,7 +303,9 @@ export const completeInstall = async ( dataplaneConfig = new AWSDataplaneConfig( awsAgentValues.accessLogARN, awsAgentValues.fullTransactionLogging, - awsAgentValues.stageTagName + awsAgentValues.stageTagName, + awsAgentValues.agentCore, + awsAgentValues.cognitoUserPoolIDs ); } else { dataplaneConfig = new DataplaneConfig('AWS'); diff --git a/src/lib/engage/utils/agents/templates/awsTemplates.ts b/src/lib/engage/utils/agents/templates/awsTemplates.ts index 64d48608..d3db3907 100644 --- a/src/lib/engage/utils/agents/templates/awsTemplates.ts +++ b/src/lib/engage/utils/agents/templates/awsTemplates.ts @@ -1,4 +1,5 @@ import { + AWSAgentCoreConfig, CentralAgentConfig, CloudFormationConfig, TraceabilityConfig, @@ -19,6 +20,9 @@ export class AWSAgentValues { centralConfig: CentralAgentConfig; traceabilityConfig: TraceabilityConfig; cloudFormationConfig: CloudFormationConfig; + agentCoreGatewayMode: boolean; + agentCore: AWSAgentCoreConfig; + cognitoUserPoolIDs: string[]; constructor(awsDeployment: string) { this.accessKey = awsDeployment === 'Other' ? '**Insert Access Key**' : ''; @@ -31,6 +35,9 @@ export class AWSAgentValues { this.centralConfig = new CentralAgentConfig(); this.traceabilityConfig = new TraceabilityConfig(); this.cloudFormationConfig = new CloudFormationConfig(); + this.agentCoreGatewayMode = false; + this.agentCore = new AWSAgentCoreConfig(); + this.cognitoUserPoolIDs = []; } updateCloudFormationConfig = () => { @@ -60,6 +67,14 @@ AWS_AUTH_SECRETKEY={{secretKey}} {{#if fullTransactionLogging}} AWS_FULLTRANSACTIONLOGGING={{fullTransactionLogging}} {{/if}} +{{#if agentCoreGatewayMode}} +AWS_GATEWAYMODE=agentcore-gateway +AWS_AGENTCORE_LOGGROUPPREFIX={{agentCore.logGroupPrefix}} +AWS_AGENTCORE_CLOUDTRAILENABLED={{agentCore.cloudTrailEnabled}} +{{#if agentCore.cloudTrailEnabled}} +AWS_AGENTCORE_CLOUDTRAILBUCKET={{agentCore.cloudTrailBucket}} +{{/if }} +{{/if}} # Amplify Central configs {{#if traceabilityConfig.usageReportingOffline}} @@ -100,8 +115,17 @@ AWS_AUTH_ACCESSKEY={{accessKey}} {{#if secretKey}} AWS_AUTH_SECRETKEY={{secretKey}} {{/if}} +{{#if agentCoreGatewayMode}} +AWS_GATEWAYMODE=agentcore-gateway +AWS_AGENTCORE_IAMAUTHENABLED={{agentCore.iamAuthEnabled}} +{{#each cognitoUserPoolIDs}} +AWS_COGNITO_USERPOOLID_{{add @index 1}}={{this}} +{{/each}} +{{else}} AWS_LOGGROUP={{logGroup}} AWS_STAGETAGNAME={{stageTagName}} +AWS_GATEWAYMODE=api-gateway +{{/if}} # Amplify Central configs CENTRAL_AGENTNAME={{centralConfig.daAgentName}} diff --git a/test/lib/engage/services/install-service/on-prem/test-aws.onprem.js b/test/lib/engage/services/install-service/on-prem/test-aws.onprem.js index 31572925..b7ac0b06 100644 --- a/test/lib/engage/services/install-service/on-prem/test-aws.onprem.js +++ b/test/lib/engage/services/install-service/on-prem/test-aws.onprem.js @@ -92,10 +92,11 @@ describe('AWS on-prem agent flow', () => { it('collects EC2 values and includes VPC-derived prompts when VPC is set', async () => { const askListResponses = [ flowModule.DeploymentTypes.EC2, - 'Yes', - 'No', + 'No', // AGENT_CORE_GATEWAY_MODE + 'Yes', // APIGWCWRoleSetup + 'No', // fullTransactionLogging 't3.micro', - 'Yes', + 'Yes', // PUBLIC_IP ]; td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); @@ -124,15 +125,17 @@ describe('AWS on-prem agent flow', () => { expect(result.logGroup).to.equal('/aws/apigw/logs'); expect(result.stageTagName).to.equal('stage-tag'); expect(result.fullTransactionLogging).to.equal(false); + expect(result.agentCoreGatewayMode).to.equal(false); expect(td.explain(promptStubs.askInput).callCount).to.equal(12); - expect(td.explain(promptStubs.askList).callCount).to.equal(5); + expect(td.explain(promptStubs.askList).callCount).to.equal(6); }); it('skips VPC-derived prompts when EC2 VPC is empty', async () => { const askListResponses = [ flowModule.DeploymentTypes.EC2, - 'Yes', - 'No', + 'No', // AGENT_CORE_GATEWAY_MODE + 'Yes', // APIGWCWRoleSetup + 'No', // fullTransactionLogging 't3.micro', ]; td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); @@ -157,14 +160,15 @@ describe('AWS on-prem agent flow', () => { expect(result.cloudFormationConfig.SecurityGroup).to.equal(''); expect(result.cloudFormationConfig.Subnet).to.equal(''); expect(td.explain(promptStubs.askInput).callCount).to.equal(10); - expect(td.explain(promptStubs.askList).callCount).to.equal(4); + expect(td.explain(promptStubs.askList).callCount).to.equal(5); }); it('collects ECS-only deployment prompts', async () => { const askListResponses = [ flowModule.DeploymentTypes.ECS_FARGATE, - 'Yes', - 'No', + 'No', // AGENT_CORE_GATEWAY_MODE + 'Yes', // APIGWCWRoleSetup + 'No', // fullTransactionLogging ]; td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); @@ -190,14 +194,15 @@ describe('AWS on-prem agent flow', () => { expect(result.cloudFormationConfig.EC2KeyName).to.equal(''); expect(logs.some((line) => line.includes('ECS Cluster Name'))).to.equal(true); expect(td.explain(promptStubs.askInput).callCount).to.equal(10); - expect(td.explain(promptStubs.askList).callCount).to.equal(3); + expect(td.explain(promptStubs.askList).callCount).to.equal(4); }); it('collects minimal prompts for OTHER deployment type', async () => { const askListResponses = [ flowModule.DeploymentTypes.OTHER, - 'Yes', - 'No', + 'No', // AGENT_CORE_GATEWAY_MODE + 'Yes', // APIGWCWRoleSetup + 'No', // fullTransactionLogging ]; td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); @@ -218,7 +223,94 @@ describe('AWS on-prem agent flow', () => { expect(result.cloudFormationConfig.ECSClusterName).to.equal(''); expect(logs.some((line) => line.includes('AWS Access Key'))).to.equal(true); expect(td.explain(promptStubs.askInput).callCount).to.equal(5); - expect(td.explain(promptStubs.askList).callCount).to.equal(3); + expect(td.explain(promptStubs.askList).callCount).to.equal(4); + }); + + it('enables agentcore gateway mode and collects a single cognito pool', async () => { + const askListResponses = [ + flowModule.DeploymentTypes.OTHER, + 'Yes', // AGENT_CORE_GATEWAY_MODE + 'Yes', // iamAuthEnabled + 'No', // enterMore? + 'Yes', // AGENTCORE_CLOUDTRAILENABLED + ]; + td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); + + const askInputResponses = [ + 'us-east-1_123456789', + '/aws/prefix', + 'my-cloudtrail-bucket', + ]; + td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); + + const result = await flowModule.gatewayConnectivity(buildInstallConfig({ isDaEnabled: true, isTaEnabled: true })); + + expect(result.agentCoreGatewayMode).to.equal(true); + expect(result.agentCore.logGroupPrefix).to.equal('/aws/prefix'); + expect(result.agentCore.iamAuthEnabled).to.equal(true); + expect(result.cognitoUserPoolIDs).to.have.length(1); + expect(result.cognitoUserPoolIDs[0]).to.equal('us-east-1_123456789'); + expect(result.agentCore.cloudTrailEnabled).to.equal(true); + expect(result.agentCore.cloudTrailBucket).to.equal('my-cloudtrail-bucket'); + expect(td.explain(promptStubs.askInput).callCount).to.equal(3); + expect(td.explain(promptStubs.askList).callCount).to.equal(5); + }); + + it('skips the log group prefix and CloudTrail prompts when TA is not enabled', async () => { + const askListResponses = [ + flowModule.DeploymentTypes.OTHER, + 'Yes', // AGENT_CORE_GATEWAY_MODE + 'No', // iamAuthEnabled + 'No', // enterMore? + ]; + td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); + + const askInputResponses = [ + 'us-east-1_999999999', + ]; + td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); + + const result = await flowModule.gatewayConnectivity(buildInstallConfig({ isDaEnabled: true, isTaEnabled: false })); + + expect(result.agentCoreGatewayMode).to.equal(true); + expect(result.agentCore.logGroupPrefix).to.equal(''); + expect(result.agentCore.cloudTrailEnabled).to.equal(false); + expect(result.agentCore.cloudTrailBucket).to.equal(''); + expect(td.explain(promptStubs.askInput).callCount).to.equal(1); + expect(td.explain(promptStubs.askList).callCount).to.equal(4); + }); + + it('enables agentcore gateway mode and collects multiple cognito pools', async () => { + const askListResponses = [ + flowModule.DeploymentTypes.OTHER, + 'Yes', // AGENT_CORE_GATEWAY_MODE + 'No', // iamAuthEnabled + 'Yes', // enterMore? (add another pool) + 'No', // enterMore? + 'No', // AGENTCORE_CLOUDTRAILENABLED + ]; + td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); + + const askInputResponses = [ + 'us-east-1_111111111', + 'eu-west-1_222222222', + '', + 'my-bucket-2', + ]; + td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); + + const result = await flowModule.gatewayConnectivity(buildInstallConfig({ isDaEnabled: true, isTaEnabled: true })); + + expect(result.agentCoreGatewayMode).to.equal(true); + expect(result.agentCore.logGroupPrefix).to.equal(''); + expect(result.agentCore.iamAuthEnabled).to.equal(false); + expect(result.cognitoUserPoolIDs).to.have.length(2); + expect(result.cognitoUserPoolIDs[0]).to.equal('us-east-1_111111111'); + expect(result.cognitoUserPoolIDs[1]).to.equal('eu-west-1_222222222'); + expect(result.agentCore.cloudTrailEnabled).to.equal(false); + expect(result.agentCore.cloudTrailBucket).to.equal('my-bucket-2'); + expect(td.explain(promptStubs.askInput).callCount).to.equal(4); + expect(td.explain(promptStubs.askList).callCount).to.equal(6); }); it('stops question flow when AWS region lookup fails', async () => { @@ -354,6 +446,9 @@ function createHelpersStubs() { this.logGroup = ''; this.region = 'us-east-1'; this.stageTagName = ''; + this.agentCoreGatewayMode = false; + this.agentCore = { logGroupPrefix: '', iamAuthEnabled: false, cloudTrailEnabled: false, cloudTrailBucket: '' }; + this.cognitoUserPoolIDs = []; this.cloudFormationConfig = { APIGWCWRoleSetup: '', APIGWTrafficLogGroupName: '/aws/apigw/logs', @@ -383,6 +478,14 @@ function createHelpersStubs() { return { AWSAgentValues, + AWSAgentCoreConfig: class AWSAgentCoreConfig { + constructor(logGroupPrefix, iamAuthEnabled, cloudTrailEnabled, cloudTrailBucket) { + this.logGroupPrefix = logGroupPrefix ?? ''; + this.iamAuthEnabled = iamAuthEnabled ?? false; + this.cloudTrailEnabled = cloudTrailEnabled ?? false; + this.cloudTrailBucket = cloudTrailBucket ?? ''; + } + }, AWSRegexPatterns: { AWS_REGEXP: /.*/, AWS_REGEXP_LOG_GROUP_NAME: /.*/, diff --git a/test/lib/engage/services/install-service/saas/test-aws.saas.js b/test/lib/engage/services/install-service/saas/test-aws.saas.js index 677bcfb9..b2685c7f 100644 --- a/test/lib/engage/services/install-service/saas/test-aws.saas.js +++ b/test/lib/engage/services/install-service/saas/test-aws.saas.js @@ -70,12 +70,13 @@ describe('AWS SaaS agent flow', () => { 'arn:aws:logs:us-east-1:000000000000:log-group:my-group', ]; td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); - const askListResponses = [ 'Assume Role Policy', engageTypes.YesNo.Yes ]; + const askListResponses = [ 'Assume Role Policy', engageTypes.YesNo.No, engageTypes.YesNo.Yes ]; td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); const result = await flowModule.AWSSaaSInstallMethods.AskGatewayQuestions(buildInstallConfig(engageTypes.GatewayTypes.AWS_GATEWAY, true)); expect(result.authType).to.equal('Assume Role Policy'); expect(result.assumeRole).to.contain('arn:aws:iam'); + expect(result.agentCoreGatewayMode).to.equal(false); expect(result.fullTransactionLogging).to.equal(true); }); @@ -132,6 +133,7 @@ describe('AWS SaaS agent flow', () => { const askListResponses = [ 'Assume Role Policy', + engageTypes.YesNo.No, engageTypes.YesNo.Yes, engageTypes.YesNo.No, engageTypes.YesNo.No, @@ -154,12 +156,73 @@ describe('AWS SaaS agent flow', () => { expect(result.redaction.maskingCharacter).to.equal('***'); }); + it('collects agentcore gateway mode with IAM auth enabled and a single cognito pool', async () => { + const askInputResponses = [ + 'arn:aws:iam::000000000000:role/name-of-role', + 'external-id', + 'stage-tag', + 'us-east-1_123456789', + '/aws/prefix', + 'my-cloudtrail-bucket', + 'arn:aws:logs:us-east-1:000000000000:log-group:my-group', + ]; + td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); + + const askListResponses = [ + 'Assume Role Policy', + engageTypes.YesNo.Yes, // AGENT_CORE_GATEWAY_MODE + engageTypes.YesNo.Yes, // AGENT_CORE_IAM_AUTH + engageTypes.YesNo.No, // ENTER_MORE_COGNITO_USER_POOL_IDS + engageTypes.YesNo.Yes, // AGENTCORE_CLOUDTRAILENABLED + engageTypes.YesNo.No, // FULL_TRANSACTION_LOGGING + ]; + td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); + + const result = await flowModule.AWSSaaSInstallMethods.AskGatewayQuestions(buildInstallConfig(engageTypes.GatewayTypes.AWS_GATEWAY, true)); + expect(result.agentCoreGatewayMode).to.equal(true); + expect(result.agentCore.logGroupPrefix).to.equal('/aws/prefix'); + expect(result.agentCore.iamAuthEnabled).to.equal(true); + expect(result.cognitoUserPoolIDs).to.have.length(1); + expect(result.cognitoUserPoolIDs[0]).to.equal('us-east-1_123456789'); + expect(result.agentCore.cloudTrailEnabled).to.equal(true); + expect(result.agentCore.cloudTrailBucket).to.equal('my-cloudtrail-bucket'); + expect(result.fullTransactionLogging).to.equal(false); + }); + + it('skips the log group prefix and CloudTrail prompts when TA is not enabled', async () => { + const askInputResponses = [ + 'arn:aws:iam::000000000000:role/name-of-role', + 'external-id', + 'stage-tag', + 'us-east-1_999999999', + ]; + td.when(promptStubs.askInput(td.matchers.anything())).thenDo(() => askInputResponses.shift()); + + const askListResponses = [ + 'Assume Role Policy', + engageTypes.YesNo.Yes, // AGENT_CORE_GATEWAY_MODE + engageTypes.YesNo.No, // AGENT_CORE_IAM_AUTH + engageTypes.YesNo.No, // ENTER_MORE_COGNITO_USER_POOL_IDS + ]; + td.when(promptStubs.askList(td.matchers.anything())).thenDo(() => askListResponses.shift()); + + const result = await flowModule.AWSSaaSInstallMethods.AskGatewayQuestions(buildInstallConfig(engageTypes.GatewayTypes.AWS_GATEWAY, false)); + expect(result.agentCoreGatewayMode).to.equal(true); + expect(result.agentCore.logGroupPrefix).to.be.undefined; + expect(result.agentCore.cloudTrailEnabled).to.be.undefined; + expect(result.agentCore.cloudTrailBucket).to.be.undefined; + expect(td.explain(promptStubs.askInput).callCount).to.equal(4); + expect(td.explain(promptStubs.askList).callCount).to.equal(4); + }); + it('builds AWS dataplane config when TA enabled', async () => { const installConfig = buildInstallConfig(engageTypes.GatewayTypes.AWS_GATEWAY, true); installConfig.gatewayConfig = { accessLogARN: 'arn:aws:logs:us-east-1:000000000000:log-group:my-group', fullTransactionLogging: true, stageTagName: 'stage-tag', + agentCore: { logGroupPrefix: '/aws/prefix', iamAuthEnabled: true }, + cognitoUserPoolIDs: [ 'us-east-1_123456789' ], redaction: {}, }; @@ -167,6 +230,8 @@ describe('AWS SaaS agent flow', () => { const dataplaneArg = td.explain(saasBaseStubs.createDataplaneResources).calls[0].args[1]; expect(dataplaneArg.type).to.equal('AWS'); expect(dataplaneArg.accessLogARN).to.contain('arn:aws:logs'); + expect(dataplaneArg.agentCore).to.deep.equal({ logGroupPrefix: '/aws/prefix', iamAuthEnabled: true }); + expect(dataplaneArg.cognitoUserPoolIDs).to.deep.equal([ 'us-east-1_123456789' ]); }); it('passes IDP config in completeInstall context', async () => {