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

feat(core): allow multiple Apps #1582

Closed
wants to merge 11 commits into from
Closed

feat(core): allow multiple Apps #1582

wants to merge 11 commits into from

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Jan 20, 2019

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.

Fixes #1479.


Pull Request Checklist

  • Testing
    • Unit test added
    • CLI change?: manually run integration tests and paste output as a PR comment
    • cdk-init template change?: coordinated update of integration tests with team
  • Docs
    • jsdocs: All public APIs documented
    • README: README and/or documentation topic updated
  • Title and Description
    • Change type: title prefixed with fix, feat will appear in changelog
    • Title: use lower-case and doesn't end with a period
    • Breaking?: last paragraph: "BREAKING CHANGE: <describe what changed + link for details>"
    • Issues: Indicate issues fixed via: "Fixes #xxx" or "Closes #xxx"
  • Sensitive Modules (requires 2 PR approvers)
    • IAM Policy Document (in @aws-cdk/aws-iam)
    • EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2)
    • Grant APIs (only if not based on official documentation with a reference)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.

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.
@rix0rrr rix0rrr requested a review from a team as a code owner January 20, 2019 21:08
@BDQ
Copy link
Contributor

BDQ commented Jan 21, 2019

From a functional perspective this looks great, and totally solves my request in #1479.

The only thing I'd note is Program seems a little generic for the name of that class, would Manager or Executor be a better?

@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jan 21, 2019

Just saw in Gitter that someone is relying on app.synthesizeStack() to do validation work for them.

Better retain that behavior then...

* True if the given construct is an App object
*/
public static isApp(construct: IConstruct): construct is App {
return (construct as any).defaultAppName !== undefined;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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'
}

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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('-');
Copy link
Contributor

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;
}

Copy link
Contributor Author

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.

Copy link
Contributor

@eladb eladb left a comment

Choose a reason for hiding this comment

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

  1. 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

*
* MyStack/Vpc/Subnet, for production stacks
*
* We do a slightly complicated calculation here because we want to
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that necessary?

Copy link
Contributor Author

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.

* True if the given construct is an App object
*/
public static isApp(construct: IConstruct): construct is App {
return (construct as any).defaultAppName !== undefined;
Copy link
Contributor

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 = {}) {
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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.
*/
Copy link
Contributor

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() {
Copy link
Contributor

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

Copy link
Contributor

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 {
Copy link
Contributor

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' }
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

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();
Copy link
Contributor

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.
Copy link
Contributor

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() {
Copy link
Contributor

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 ');
Copy link
Contributor

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 || {}) }
Copy link
Contributor

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).

@eladb
Copy link
Contributor

eladb commented Feb 4, 2019

@rix0rrr any updates on this?

@rix0rrr rix0rrr self-assigned this Feb 4, 2019
@rix0rrr rix0rrr closed this Mar 4, 2019
@rix0rrr rix0rrr deleted the huijbers/app-is-app branch April 23, 2019 07:12
@NGL321 NGL321 added the contribution/core This is a PR that came from AWS. label Sep 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for multiple instances of a cdk.App
6 participants