Skip to content

Commit

Permalink
feat: support creating multiple stacks in the same AWS account (#194)
Browse files Browse the repository at this point in the history
* refactor: support additional context option for stack environment name

* feat: allow setting a label for the stack name

* docs: add info about stack label value

* refactor: move stack label title case to local variable

* refactor: set secret name correctly

* refactor: handle dynamic secret names in Lambda functions
  • Loading branch information
hendrickson-tyler authored Jun 26, 2024
1 parent c0a89ce commit 236e3f5
Show file tree
Hide file tree
Showing 19 changed files with 134 additions and 40 deletions.
1 change: 1 addition & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
write-mode: overwrite
contents: |
{
"stackLabel": "dev",
"amazonConnect": {
"instanceArn": "placeholder",
"securityKeyId": "placeholder",
Expand Down
20 changes: 17 additions & 3 deletions bin/c3-amazon-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import { exec } from 'child_process';

(async () => {
const stackVersion = await getMostRecentGitTag();
console.log(`Deploying stack version ${stackVersion}...`);

const app = new App();
new C3AmazonConnectStack(app, 'C3AmazonConnectStack', {
const stackLabel = app.node.tryGetContext('stackLabel') as string;
console.log(`Deploying stack version ${stackVersion} for "${stackLabel}"...`);

const formattedStackLabel = getFormattedStackLabel(stackLabel);
new C3AmazonConnectStack(app, `C3AmazonConnect${formattedStackLabel}Stack`, {
description: `Stack containing the resources for C3 for Amazon Connect (${stackVersion}).`,
});
})();

/**
* Fetches the most recent git tag in the repository.
*
* @returns {Promise<string>} The most recent git tag in the repository
*/
Expand All @@ -32,3 +35,14 @@ async function getMostRecentGitTag(): Promise<string> {
return 'v?.?.?';
}
}

/**
* Gets the stack label from the context, formatted as a title.
*
* @param stackLabel The stack label from the context.
* @returns A formatted stack label.
*/
function getFormattedStackLabel(stackLabel: string): string {
const lowerCaseValue = stackLabel.toLowerCase();
return lowerCaseValue.charAt(0).toUpperCase() + lowerCaseValue.slice(1);
}
1 change: 1 addition & 0 deletions cdk.context.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"stackLabel": "",
"amazonConnect": {
"instanceArn": "",
"securityKeyId": "",
Expand Down
4 changes: 4 additions & 0 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ In order to facilitate this process, you will need to provide some values to the

#### Configuration Values

| Value | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `stackLabel` | **Optional**. A unique label to give to the deployed stack. To enable multiple stacks to be deployed to a single AWS account, this field must be populated with a unique name. |

##### Amazon Connect

| Value | Description |
Expand Down
55 changes: 40 additions & 15 deletions lib/c3-amazon-connect-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class C3AmazonConnectStack extends Stack {
private c3AppUrlFragment: string;

// Context variables.
private stackLabel: string;
private amazonConnectContext: AmazonConnectContext;
private c3Context: C3Context;
private featuresContext: FeaturesContext;
Expand Down Expand Up @@ -129,6 +130,8 @@ export class C3AmazonConnectStack extends Stack {
* Ensures that all required context variables are set. Throws an error if any are missing.
*/
private validateContextVariables(): void {
this.stackLabel = this.node.tryGetContext('stackLabel');

this.amazonConnectContext = this.node.tryGetContext('amazonConnect');
validateAmazonConnectContext(this.amazonConnectContext);

Expand Down Expand Up @@ -202,10 +205,13 @@ export class C3AmazonConnectStack extends Stack {
*/
private createC3ApiKeySecret(): void {
console.log('Creating secret for C3 API key...');
const secretLabel = this.stackLabel
? `_${this.stackLabel.toUpperCase()}`
: '';
this.c3ApiKeySecret = new Secret(this, 'C3APIKey', {
secretName: 'C3_API_KEY',
secretName: 'C3_API_KEY' + secretLabel,
secretStringValue: SecretValue.unsafePlainText('<Your C3 API key>'),
description: 'The API key used for C3 payments.',
description: 'The API key used for C3 Payment.',
});
}

Expand All @@ -217,8 +223,11 @@ export class C3AmazonConnectStack extends Stack {
*/
private createPrivateKeySecret(): void {
console.log('Creating private key secret...');
const secretLabel = this.stackLabel
? `_${this.stackLabel.toUpperCase()}`
: '';
this.privateKeySecret = new Secret(this, 'C3PrivateKey', {
secretName: 'C3_PRIVATE_KEY',
secretName: 'C3_PRIVATE_KEY' + secretLabel,
secretStringValue: SecretValue.unsafePlainText(
'<The content of your private key>',
),
Expand All @@ -243,8 +252,9 @@ export class C3AmazonConnectStack extends Stack {
join(__dirname, 'lambda/c3-create-payment-request'),
),
environment: {
C3_VENDOR_ID: this.c3Context.vendorId,
C3_BASE_URL: this.c3BaseUrl,
C3_VENDOR_ID: this.c3Context.vendorId,
C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName,
LOGO_URL: this.logoUrl,
SUPPORT_PHONE: this.supportPhone,
SUPPORT_EMAIL: this.supportEmail,
Expand Down Expand Up @@ -277,6 +287,7 @@ export class C3AmazonConnectStack extends Stack {
code: Code.fromAsset(join(__dirname, 'lambda/c3-tokenize-transaction')),
environment: {
C3_ENV: this.c3Context.env,
C3_PRIVATE_KEY_SECRET_ID: this.privateKeySecret.secretName,
C3_PAYMENT_GATEWAY: this.c3Context.paymentGateway,
CONNECT_SECURITY_KEY_ID: this.amazonConnectContext.securityKeyId,
},
Expand All @@ -299,7 +310,12 @@ export class C3AmazonConnectStack extends Stack {
// Create additional payment gateway secrets and add to policy.
switch (this.c3Context.paymentGateway) {
case C3PaymentGateway.Zift:
new Zift(this, getSecretValuePolicy);
new Zift(
this,
this.tokenizeTransactionFunction,
getSecretValuePolicy,
this.stackLabel,
);
break;
default:
throw new Error(
Expand All @@ -322,6 +338,7 @@ export class C3AmazonConnectStack extends Stack {
code: Code.fromAsset(join(__dirname, 'lambda/c3-submit-payment')),
environment: {
C3_BASE_URL: this.c3BaseUrl,
C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName,
},
codeSigningConfig: this.codeSigningConfig,
});
Expand All @@ -347,6 +364,7 @@ export class C3AmazonConnectStack extends Stack {
code: Code.fromAsset(join(__dirname, 'lambda/c3-email-receipt')),
environment: {
C3_BASE_URL: this.c3BaseUrl,
C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName,
},
codeSigningConfig: this.codeSigningConfig,
});
Expand Down Expand Up @@ -390,18 +408,25 @@ export class C3AmazonConnectStack extends Stack {
}

// Create the app.
const application = new CfnApplication(this, 'C3ConnectApp', {
name: 'Payment Request',
namespace: 'c3-payment',
description: 'Agent application for collecting payments with C3.',
permissions: ['User.Details.View', 'Contact.Details.View'],
applicationSourceConfig: {
externalUrlConfig: {
accessUrl: `https://${this.c3Context.vendorId}.${this.c3AppUrlFragment}/agent-workspace?contactCenter=amazon&instanceId=${instanceId}&region=${region}${agentAssistedIVRParams}${configuredFeatureParams}`,
approvedOrigins: [], // Don't allow any other origins.
const stackLabelTitleCase =
this.stackLabel.charAt(0).toUpperCase() + this.stackLabel.slice(1);
const appLabel = this.stackLabel ? ` - ${stackLabelTitleCase}` : '';
const application = new CfnApplication(
this,
`C3ConnectApp${stackLabelTitleCase}`,
{
name: 'Payment Request' + appLabel, // App name is unfortunately required to be unique to create.
namespace: `c3-payment-${this.stackLabel}`,
description: 'Agent application for collecting payments with C3.',
permissions: ['User.Details.View', 'Contact.Details.View'],
applicationSourceConfig: {
externalUrlConfig: {
accessUrl: `https://${this.c3Context.vendorId}.${this.c3AppUrlFragment}/agent-workspace?contactCenter=amazon&instanceId=${instanceId}&region=${region}${agentAssistedIVRParams}${configuredFeatureParams}`,
approvedOrigins: [], // Don't allow any other origins.
},
},
},
});
);

// Associate the app with the Amazon Connect instance.
new CfnIntegrationAssociation(this, `C3ConnectIntegrationApp`, {
Expand Down
1 change: 1 addition & 0 deletions lib/features/agent-assisted-payment-ivr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class AgentAssistedPaymentIVR {
environment: {
C3_ENV: c3Context.env,
C3_BASE_URL: this.c3BaseUrl,
C3_API_KEY_SECRET_ID: this.c3ApiKeySecret.secretName,
},
codeSigningConfig: this.codeSigningConfig,
},
Expand Down
10 changes: 7 additions & 3 deletions lib/lambda/c3-create-payment-request/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ export async function handler(event) {
if (!c3ApiKey) {
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'C3_API_KEY',
SecretId: process.env.C3_API_KEY_SECRET_ID,
});
c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand))
.SecretString;
if (!c3ApiKey) {
throw new Error('No value found for C3_API_KEY secret.');
throw new Error(
`No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`,
);
}
if (c3ApiKey === '<Your C3 API key>') {
throw new Error('Value for C3_API_KEY secret is not set.');
throw new Error(
`Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`,
);
}
} else {
console.log('Using API key in memory.');
Expand Down
12 changes: 8 additions & 4 deletions lib/lambda/c3-email-receipt/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,25 @@ export async function handler(event) {
if (!c3ApiKey) {
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'C3_API_KEY',
SecretId: process.env.C3_API_KEY_SECRET_ID,
});
c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand))
.SecretString;
if (!c3ApiKey) {
throw new Error('No value found for C3_API_KEY secret.');
throw new Error(
`No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`,
);
}
if (c3ApiKey === '<Your C3 API key>') {
throw new Error('Value for C3_API_KEY secret is not set.');
throw new Error(
`Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`,
);
}
} else {
console.log('Using API key in memory.');
}

// Email the receipt
// Email the receipt.
const transactionId = contactAttributes.TransactionId;
const emailAddress = contactAttributes.Email;
await emailReceipt(transactionId, emailAddress);
Expand Down
10 changes: 7 additions & 3 deletions lib/lambda/c3-send-agent-message/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ export async function handler(event) {
if (!c3ApiKey) {
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'C3_API_KEY',
SecretId: process.env.C3_API_KEY_SECRET_ID,
});
c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand))
.SecretString;
if (!c3ApiKey) {
throw new Error('No value found for C3_API_KEY secret.');
throw new Error(
`No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`,
);
}
if (c3ApiKey === '<Your C3 API key>') {
throw new Error('Value for C3_API_KEY secret is not set.');
throw new Error(
`Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`,
);
}
} else {
console.log('Using API key in memory.');
Expand Down
10 changes: 7 additions & 3 deletions lib/lambda/c3-submit-payment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@ export async function handler(event) {
if (!c3ApiKey) {
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'C3_API_KEY',
SecretId: process.env.C3_API_KEY_SECRET_ID,
});
c3ApiKey = (await secretsManagerClient.send(getSecretValueCommand))
.SecretString;
if (!c3ApiKey) {
throw new Error('No value found for C3_API_KEY secret.');
throw new Error(
`No value found for ${process.env.C3_API_KEY_SECRET_ID} secret.`,
);
}
if (c3ApiKey === '<Your C3 API key>') {
throw new Error('Value for C3_API_KEY secret is not set.');
throw new Error(
`Value for ${process.env.C3_API_KEY_SECRET_ID} secret is not set.`,
);
}
} else {
console.log('Using API key in memory.');
Expand Down
12 changes: 7 additions & 5 deletions lib/lambda/c3-tokenize-transaction/gateways/zift.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,36 @@ export class Zift {
}
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'ZIFT_CREDENTIALS',
SecretId: process.env.ZIFT_CREDENTIALS_SECRET_ID,
});
ziftCredentials = JSON.parse(
(await secretsManagerClient.send(getSecretValueCommand)).SecretString,
);
if (!ziftCredentials) {
throw new Error('No value found for ZIFT_CREDENTIALS secret.');
throw new Error(
`No value found for ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret.`,
);
}
if (
!ziftCredentials.accountId ||
ziftCredentials.accountId === '<Your Zift account ID>'
) {
throw new Error(
'Value for accountId in ZIFT_CREDENTIALS secret is not set.',
`Value for accountId in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`,
);
} else if (
!ziftCredentials.username ||
ziftCredentials.username === '<Your Zift username>'
) {
throw new Error(
'Value for username in ZIFT_CREDENTIALS secret is not set.',
`Value for username in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`,
);
} else if (
!ziftCredentials.password ||
ziftCredentials.password === '<Your Zift password>'
) {
throw new Error(
'Value for password in ZIFT_CREDENTIALS secret is not set.',
`Value for password in ${process.env.ZIFT_CREDENTIALS_SECRET_ID} secret is not set.`,
);
}
}
Expand Down
10 changes: 7 additions & 3 deletions lib/lambda/c3-tokenize-transaction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,19 @@ export async function handler(event) {
if (!c3PrivateKey) {
const secretsManagerClient = new SecretsManagerClient();
const getSecretValueCommand = new GetSecretValueCommand({
SecretId: 'C3_PRIVATE_KEY',
SecretId: process.env.C3_PRIVATE_KEY_SECRET_ID,
});
c3PrivateKey = (await secretsManagerClient.send(getSecretValueCommand))
.SecretString;
if (!c3PrivateKey) {
throw new Error('No value found for C3_PRIVATE_KEY secret.');
throw new Error(
`No value found for ${process.env.C3_PRIVATE_KEY_SECRET_ID} secret.`,
);
}
if (c3PrivateKey === '<The content of your private key>') {
throw new Error('Value for C3_PRIVATE_KEY secret is not set.');
throw new Error(
`Value for ${process.env.C3_PRIVATE_KEY_SECRET_ID} secret is not set.`,
);
}
} else {
console.log('Using private key in memory.');
Expand Down
1 change: 1 addition & 0 deletions lib/models/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { C3Context } from './c3-context';
import { FeaturesContext } from './features-context';

export interface Context {
stackLabel: string;
amazonConnect: AmazonConnectContext;
c3: C3Context;
logoUrl: string;
Expand Down
Loading

0 comments on commit 236e3f5

Please sign in to comment.