-
Notifications
You must be signed in to change notification settings - Fork 4k
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
feat(core): allow multiple Apps #1582
Conversation
Change the meaning of `App` to represent the application the user is defining (as opposed to the internals of the CDK program). The names of Apps (if given) will be incorporated into the Stack name, so that it becomes possible to instantiate your Stacks multiple times just like other constructs (without having to make the `id` parameter unique). A new stack property `stackName` exists to force the stack to a different name. If a single `App` is instantiated without parameters, the observable behavior of your CDK app won't change. If you insantiate more than one `App`, all of them must receive ids. To represent the CDK internals, a new class called `Program` exists, which the user doesn't need to interact with. `app.run()` is no longer necessary, and is deprecated. It still exists to avoid breaking existing programs, but will be removed in a future release.
From a functional perspective this looks great, and totally solves my request in #1479. The only thing I'd note is |
Just saw in Gitter that someone is relying on Better retain that behavior then... |
packages/@aws-cdk/cdk/lib/app.ts
Outdated
* True if the given construct is an App object | ||
*/ | ||
public static isApp(construct: IConstruct): construct is App { | ||
return (construct as any).defaultAppName !== undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we were to use Symbols, would that mess with JSII? It would be a safer way to make this determination.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure.
Also, what would the advantage of a symbol be over any instance of an object? Effectively,
const sym1 = Symbol();
const sym2 = {};
sym1
and sym2
would behave the same when compared for equality, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Symbols are unique, therefore there is no way to accidentally add add a 'defaultAppName' property to your construct. They can also be used to compensate for instanceOf
shortcomings:
const isApp = Symbol.for('aws-cdk:isApp'); // creates or gets a universal named symbol from the registry
class App {
[isApp]: true; // will not collide with an ordinary property like 'isApp'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see, as keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That will not across JSII, for sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need to? We only need this deep in the internals to tag App, Stack and Construct.. we can just generate an ordinary property in the JSII facades. It eliminates instanceOf and name-collisions when checking properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for symbols
return this.node.rootPath() | ||
.filter(c => !App.isDefaultApp(c)) | ||
.map(c => c.node.id) | ||
.join('-'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I reading this wrong? rootPath
removes the first element so it won't include the App? Your tests pass so I assume so :)
public rootPath(): IConstruct[] {
const ancestors = this.ancestors();
ancestors.shift();
return ancestors;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Program
is the actual root of the construct tree, so that's the one that gets cut.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I rather we opt in for auto-run through the existence of CDK_OUT (happy to rename) than opt out with a new environment variable. The current approach will no doubt trip people off in very weird ways, especially in Jsii languages.
- if you are inclined (but ok if we punt), would like to discuss a way to generalize synthesis and move to Construct
packages/@aws-cdk/aws-ec2/lib/vpc.ts
Outdated
* | ||
* MyStack/Vpc/Subnet, for production stacks | ||
* | ||
* We do a slightly complicated calculation here because we want to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to have to rerun all integration tests that use EC2 capacity in all projects. Takes forever.
packages/@aws-cdk/cdk/lib/app.ts
Outdated
* True if the given construct is an App object | ||
*/ | ||
public static isApp(construct: IConstruct): construct is App { | ||
return (construct as any).defaultAppName !== undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for symbols
constructor() { | ||
super(); | ||
this.loadContext(); | ||
constructor(id?: string, props: AppProps = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we are breaking the construct signature anyway here, why not make ID and optional prop instead of an optional argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cf:
const app = new App('MyApp');
const app = new App({ id: 'MyApp' });
Even though we don't have the full construct signature, at least in this it's somewhat obvious that the ID passed is a regular construct ID, which will contribute to a construct path (a stack would have path /MyApp/MyStack
). I don't find that as obvious in the second case. Plus, the second one reads more noisily. Just generally don't care for it much.
return s; | ||
} | ||
function numberOfAppsInProgram(app: IConstruct): number { | ||
return findRoot(app).node.findAll().filter(App.isApp).length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t we have node.root?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't but it's easy enough to make.
@@ -179,6 +193,16 @@ export class Stack extends Construct { | |||
} | |||
} | |||
|
|||
/** | |||
* The environment in which this stack is deployed. | |||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why couldn’t me make this eager?
/** | ||
* Initialize the default Program instance | ||
*/ | ||
private initializeDefaultProgram() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d give this method a more specific name: “autoRunOnExit” or something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest simply runBeforeExit
.
} | ||
} | ||
|
||
interface ISynthesizable extends IConstruct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve been wanting to add this at the construct level for ages. Would you be open to discussing?
await shell(testCommand, timers); | ||
await shell(testCommand, { | ||
timers, | ||
env: { CDK_TEST_MODE: '1' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concerns me. I actually rather we opt in from the toolkit to auto run the app on exit instead of opt out, or perhaps base it on the existence of CDK_OUT?
@@ -37,12 +37,13 @@ async function main() { | |||
const searchDir = path.dirname(appletFile); | |||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdkapplet')); | |||
try { | |||
const app = new cdk.App(); | |||
const program = new cdk.Program(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be a cdk.Host
, too, for example... (Chipping in on the naming stuff is hard :D)
|
||
for (const [name, definition] of Object.entries(appletMap)) { | ||
await constructStack(app, searchDir, tempDir, name, definition); | ||
} | ||
app.run(); | ||
program.run(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we making sure you cannot add new app's on there after run
was called? (Mentioning here because I see it now)
* | ||
* @default Automatically determined | ||
*/ | ||
env?: Environment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we're breaking stuff, can we rename to environment
? This is infrequently used and the abbreviation doesn't add that much...
* True if the given construct is a default-named App | ||
*/ | ||
public static isDefaultApp(construct: IConstruct) { | ||
return App.isApp(construct) && construct.defaultAppName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't return true
(per the documentation), but it'll return truthy. I'd coerce to a boolean context.
*/ | ||
export class App extends Construct { | ||
/** | ||
* True if the given construct is a default-named App |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we get consistent about having:
@param
for each parameter@returns
for the return value
Missing those, documentation will be very poor, especially for other languages such as Java
.
|
||
this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme()); | ||
this.name = this.node.id; | ||
this.name = props.stackName !== undefined ? props.stackName : this.calculateStackName(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
props.stackName || this.calculateStackName()
?
import { validateAndThrow } from './util/validation'; | ||
|
||
/** | ||
* Represents a CDK program. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Program and app feel too interchangeable. Would rather call this Host
or something more distinctive. No hard feelings though.
/** | ||
* Initialize the default Program instance | ||
*/ | ||
private initializeDefaultProgram() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest simply runBeforeExit
.
export function validateAndThrow(construct: IConstruct) { | ||
const errors = construct.node.validateTree(); | ||
if (errors.length > 0) { | ||
const errorList = errors.map(e => `[${e.source.node.path}] ${e.message}`).join('\n '); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like using \t
for indents in messages because it gives users more control over how it renders (it's more accessible).
|
||
await makeShellScriptExecutable(command[0]); | ||
|
||
const child = child_process.spawn(command[0], command.slice(1), { | ||
// Need this for Windows where we want .cmd and .bat to be found as well. | ||
shell: true, | ||
stdio: [ 'ignore', 'pipe', 'inherit' ] | ||
stdio: [ 'ignore', 'pipe', 'inherit' ], | ||
env: { ...process.env, ...(options.env || {}) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc should mention that options.env
is additive on top of process.env
.
I don't believe this is the usual behavior (typically I'd expect it uses process.env
if I don't pass anything, and whatever I passed otherwise).
@rix0rrr any updates on this? |
Change the meaning of
App
to represent the application the useris defining (as opposed to the internals of the CDK program). The
names of Apps (if given) will be incorporated into the Stack name,
so that it becomes possible to instantiate your Stacks multiple
times just like other constructs (without having to make the
id
parameter unique). A new stack propertystackName
existsto force the stack to a different name.
If a single
App
is instantiated without parameters, the observablebehavior of your CDK app won't change. If you insantiate more than
one
App
, all of them must receive ids.To represent the CDK internals, a new class called
Program
exists,which the user doesn't need to interact with.
app.run()
is no longer necessary, and is deprecated. It stillexists to avoid breaking existing programs, but will be removed
in a future release.
Fixes #1479.
Pull Request Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.