Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pass valid props to root stack #1675

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/chatty-gifts-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend': minor
---

Allow StackProps to be passed from defineBackend down to AmplifyStack for root Stack configuration
11 changes: 10 additions & 1 deletion packages/backend/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ResourceProvider } from '@aws-amplify/plugin-types';
import { SsmEnvironmentEntriesGenerator } from '@aws-amplify/plugin-types';
import { SsmEnvironmentEntry } from '@aws-amplify/plugin-types';
import { Stack } from 'aws-cdk-lib';
import { StackProps } from 'aws-cdk-lib';

export { a }

Expand Down Expand Up @@ -70,7 +71,12 @@ export { ConstructFactoryGetInstanceProps }
export { defineAuth }

// @public
export const defineBackend: <T extends DefineBackendProps>(constructFactories: T) => Backend<T>;
export const defineBackend: <T extends DefineBackendProps>(constructFactories: T, props?: DefineBackendOptions) => Backend<T>;

// @public (undocumented)
export type DefineBackendOptions = {
mainStackProps?: MainStackProps;
};

// @public (undocumented)
export type DefineBackendProps = Record<string, ConstructFactory<ResourceProvider & Partial<ResourceAccessAcceptorFactory<never>>>> & {
Expand All @@ -89,6 +95,9 @@ export { GenerateContainerEntryProps }

export { ImportPathVerifier }

// @public
export type MainStackProps = Pick<StackProps, 'env' | 'crossRegionReferences'>;

export { ResourceProvider }

// @public
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@aws-amplify/plugin-types';
import { Stack } from 'aws-cdk-lib';
import { ClientConfig } from '@aws-amplify/client-config';
import { MainStackProps } from './engine/amplify_stack.js';

export type BackendBase = {
createStack: (name: string) => Stack;
Expand All @@ -22,6 +23,10 @@ export type DefineBackendProps = Record<
>
> & { [K in keyof BackendBase]?: never };

export type DefineBackendOptions = {
mainStackProps?: MainStackProps;
};

/**
* Use `defineBackend` to create an instance of this type.
* This object has the Amplify BackendBase methods on it for interacting with the backend.
Expand Down
42 changes: 38 additions & 4 deletions packages/backend/src/backend_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { createDefaultStack } from './default_stack_factory.js';
import { getBackendIdentifier } from './backend_identifier.js';
import { platformOutputKey } from '@aws-amplify/backend-output-schemas';
import { fileURLToPath } from 'node:url';
import { Backend, DefineBackendProps } from './backend.js';
import {
Backend,
DefineBackendOptions,
DefineBackendProps,
} from './backend.js';
import { AmplifyBranchLinkerConstruct } from './engine/branch-linker/branch_linker_construct.js';
import {
ClientConfig,
Expand All @@ -35,6 +39,9 @@ const rootStackTypeIdentifier = 'root';
const DEFAULT_CLIENT_CONFIG_VERSION_FOR_BACKEND_ADD_OUTPUT =
ClientConfigVersionOption.V1;

// Stricter Omit for constructor overloading
type ExplicitOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

/**
* Factory that collects and instantiates all the Amplify backend constructs
*/
Expand All @@ -51,11 +58,37 @@ export class BackendFactory<

private readonly stackResolver: StackResolver;
private readonly customOutputsAccumulator: CustomOutputsAccumulator;

/**
* stack and props.mainStackProps are mutually exclusive
*/
constructor(
constructFactories: T,
stack: Stack,
props?: ExplicitOmit<DefineBackendOptions, 'mainStackProps'>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does a type of never here not work?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never would prevent passing anything in props, but only mainStackProps conflicts with a passed Stack. While there are currently no other props in the DefineBackendOptions type, @sobolk requested this implementation for the further expansion of the API and backend options could potentially be used for anything in the process tree, not only for the root Stack config. Semantically it also makes sense to allow DefineBackendOptions to be passed to defineBackend even if a Stack is passed, as long as those options aren't specific to the root Stack definition.

FYI I did try with built-in Omit, but it's not explicit so doesn't prevent passing mainStackProps. Hence the ExplicitOmit implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should maybe change the props key to options though, for consistency sake. Thoughts?

);
/**
*
*/
constructor(
constructFactories: T,
stack?: never,
props?: DefineBackendOptions
);

/**
* Initialize an Amplify backend with the given construct factories and in the given CDK App.
* If no CDK App is specified a new one is created
*/
constructor(constructFactories: T, stack: Stack = createDefaultStack()) {
constructor(
constructFactories: T,
stack?: Stack,
props?: DefineBackendOptions
) {
if (stack === undefined) {
stack = createDefaultStack(undefined, props?.mainStackProps);
}

new AttributionMetadataStorage().storeAttributionMetadata(
stack,
rootStackTypeIdentifier,
Expand Down Expand Up @@ -150,9 +183,10 @@ export class BackendFactory<
* @param constructFactories - list of backend factories such as those created by `defineAuth` or `defineData`
*/
export const defineBackend = <T extends DefineBackendProps>(
constructFactories: T
constructFactories: T,
props?: DefineBackendOptions
): Backend<T> => {
const backend = new BackendFactory(constructFactories);
const backend = new BackendFactory(constructFactories, undefined, props);
return {
...backend.resources,
createStack: backend.createStack,
Expand Down
11 changes: 9 additions & 2 deletions packages/backend/src/default_stack_factory.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { App, Stack } from 'aws-cdk-lib';
import { ProjectEnvironmentMainStackCreator } from './project_environment_main_stack_creator.js';
import { getBackendIdentifier } from './backend_identifier.js';
import { MainStackProps } from './engine/amplify_stack.js';

/**
* Creates a default CDK scope for the Amplify backend to use if no scope is provided to the constructor
*/
export const createDefaultStack = (app = new App()): Stack => {
export const createDefaultStack = (
app?: App,
props?: MainStackProps
): Stack => {
if (app === undefined) {
app = new App();
}
const mainStackCreator = new ProjectEnvironmentMainStackCreator(
app,
getBackendIdentifier(app)
);
return mainStackCreator.getOrCreateMainStack();
return mainStackCreator.getOrCreateMainStack(props);
};
29 changes: 26 additions & 3 deletions packages/backend/src/engine/amplify_stack.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import { AmplifyFault } from '@aws-amplify/platform-core';
import { Aspects, CfnElement, IAspect, Stack } from 'aws-cdk-lib';
import { Aspects, CfnElement, IAspect, Stack, StackProps } from 'aws-cdk-lib';
import { Role } from 'aws-cdk-lib/aws-iam';
import { Construct, IConstruct } from 'constructs';

/**
* Props for root CDK Stack
* @see {@link https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackProps.html | AWS documentation for aws-cdk-lib.StackProps}
*
* Props are picked to ensure explicit addition of new StackProps is required.
* Props incompatible with Amplify's intended Stack hierarchy, build or deployment processes should always be ommited:
* - stackName: Conflicts with dynamic resource naming.
* - synthesizer: Conflicts with managed deployments and resource references.
* - terminationProtection: Conflicts with sandbox/app delete.
* - permissionsBoundary: Conflicts with single root Stack ethos (i.e. Unable to create Role prior to `defineBackend`).
*
* Props are passed down from `defineBackend`:
* @example <caption>Set explicit region (e.g. for `new cloudfront.experimental.EdgeFunction`)</caption>
* ```
* defineBackend({}, {
* env:
* region: 'us-east-1' // Any valid AWS region
* }
* })
* ```
*/
export type MainStackProps = Pick<StackProps, 'env' | 'crossRegionReferences'>;

/**
* Amplify-specific Stack implementation to handle cross-cutting concerns for all Amplify stacks
*/
export class AmplifyStack extends Stack {
/**
* Default constructor
*/
constructor(scope: Construct, id: string) {
super(scope, id);
constructor(scope: Construct, id: string, props?: MainStackProps) {
super(scope, id, props);
Aspects.of(this).add(new CognitoRoleTrustPolicyValidator());
}
/**
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { type MainStackProps } from './engine/amplify_stack.js';
export { defineBackend } from './backend_factory.js';
export * from './backend.js';
export * from './secret.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BackendIdentifier, MainStackCreator } from '@aws-amplify/plugin-types';
import { Construct } from 'constructs';
import { Stack, Tags } from 'aws-cdk-lib';
import { AmplifyStack } from './engine/amplify_stack.js';
import { AmplifyStack, MainStackProps } from './engine/amplify_stack.js';
import { BackendIdentifierConversions } from '@aws-amplify/platform-core';

/**
Expand All @@ -20,11 +20,12 @@ export class ProjectEnvironmentMainStackCreator implements MainStackCreator {
/**
* Get a stack for this environment in the provided CDK scope
*/
getOrCreateMainStack = (): Stack => {
getOrCreateMainStack = (props?: MainStackProps): Stack => {
if (this.mainStack === undefined) {
this.mainStack = new AmplifyStack(
this.scope,
BackendIdentifierConversions.toStackName(this.backendId)
BackendIdentifierConversions.toStackName(this.backendId),
props
);
}

Expand Down