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(servicecatalogappregistry): application-associator L2 Construct #22024

Merged
merged 5 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 71 additions & 5 deletions packages/@aws-cdk/aws-servicecatalogappregistry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@

<!--END STABILITY BANNER-->

[AWS Service Catalog App Registry](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/appregistry.html)
[AWS Service Catalog App Registry](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/appregistry.html)
enables organizations to create and manage repositores of applications and associated resources.

## Table Of Contents

- [Application](#application)
- [Automatic-Application](#automatic-application)
- [Attribute-Group](#attribute-group)
- [Associations](#associations)
- [Associating application with an attribute group](#attribute-group-association)
Expand All @@ -44,11 +45,11 @@ import * as appreg from '@aws-cdk/aws-servicecatalogappregistry';
## Application

An AppRegistry application enables you to define your applications and associated resources.
The application name must be unique at the account level, but is mutable.
The application name must be unique at the account level and it's immutable.

```ts
const application = new appreg.Application(this, 'MyFirstApplication', {
applicationName: 'MyFirstApplicationName',
applicationName: 'MyFirstApplicationName',
description: 'description for my application', // the description is optional
});
```
Expand All @@ -64,14 +65,79 @@ const importedApplication = appreg.Application.fromApplicationArn(
);
```

## Automatic-Application

An AppRegistry L2 construct to automatically create an application with the given name and description.
The application name must be unique at the account level and it's immutable.
`AutomaticApplication` L2 construct will automatically associate all stacks in the given scope, however
in case of a `Pipeline` stack, stage underneath the pipeline will not automatically be associated and
needs to be associated separately.
If cross account stack is detected, then this construct will automatically share the application to consumer accounts.
Cross account feature will only work for non environment agnostic stacks.

Following will create an Application named `MyAutoApplication` in account `123456789012` and region `us-east-1`
and will associate all stacks in the `App` scope to `MyAutoApplication`.

```ts
const app = new App();
const autoApp = new appreg.AutomaticApplication(app, 'AutoApplication', {
applicationName: 'MyAutoApplication',
description: 'Testing auto application',
stackProps: {
stackName: 'MyAutoApplicationStack',
env: {account: '123456789012', region: 'us-east-1'},
},
});
```

In case of a Pipeline stack, you need to pass the reference of `AutomaticApplication` to pipeline stack and associate
each stage as shown below:

```ts
import * as cdk from "@aws-cdk/core";
import * as codepipeline from "@aws-cdk/pipelines";
import * as codecommit from "@aws-cdk/aws-codecommit";
declare const repo: codecommit.Repository;
declare const pipeline: codepipeline.CodePipeline;
declare const beta: cdk.Stage;
class ApplicationPipelineStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: ApplicationPipelineStackProps) {
super(scope, id, props);

//associate the stage to automatic application.
props.application.associateStage(beta, this.stackName);
pipeline.addStage(beta);
}
};

interface ApplicationPipelineStackProps extends cdk.StackProps {
application: appreg.AutomaticApplication;
};

const app = new App();
const autoApp = new appreg.AutomaticApplication(app, 'AutoApplication', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Per what I see in the code, AutomaticApplication is a Construct. Construct's scope should be a Stack, not App. I think it makes more sense to have AppRegistry application to be part of the component's toolchain. The toolchain contains anything related to software development life cycle of the component. Some examples: 1/ pull request validation in trunk-based development model 2/ tenant provisioning logical unit in multi-tenant SaaS applications using silo deployment model. Here is an example of a toolchain containing continuous deployment and pull request validation logical units: https:/alexpulver/usermanagement-backend/blob/main/toolchain.py

Copy link
Contributor

Choose a reason for hiding this comment

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

An approach similar to this AWS AppConfig blog post which is aligned with AWS mutli-account recommendations.

Copy link
Contributor

Choose a reason for hiding this comment

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

Construct's scope should be a Stack, not App.

I don't agree with this necessarily. A construct's scope can be any other construct, including an App. Yes, CFN resources ultimately need to be created in the (transitive) scope of a Stack, but this thing can be an exception.

Having said that, I think you actually mean something else. I think you mean something along the lines of:

In the case of a Pipeline deployment, the Application should be created in the scope of the Pipeline Stack/Toolchain stack, not at the top level.

I tend to agree that's what most people would probably want. But wouldn't that just mean you don't use AutomaticApplication, but use a plain old Application instead?

applicationName: 'MyPipelineAutoApplication',
description: 'Testing pipeline auto app',
stackProps: {
stackName: 'MyPipelineAutoApplicationStack',
env: {account: '123456789012', region: 'us-east-1'},
},
});

const cdkPipeline = new ApplicationPipelineStack(app, 'CDKApplicationPipelineStack', {
application: autoApp,
env: {account: '123456789012', region: 'us-east-1'},
});
```

## Attribute Group

An AppRegistry attribute group acts as a container for user-defined attributes for an application.
Metadata is attached in a machine-readble format to integrate with automated workflows and tools.

```ts
const attributeGroup = new appreg.AttributeGroup(this, 'MyFirstAttributeGroup', {
attributeGroupName: 'MyFirstAttributeGroupName',
attributeGroupName: 'MyFirstAttributeGroupName',
description: 'description for my attribute group', // the description is optional,
attributes: {
project: 'foo',
Expand Down Expand Up @@ -104,7 +170,7 @@ Resources are CloudFormation stacks that you can associate with an application t
stacks together to enable metadata rich insights into your applications and resources.
A Cloudformation stack can only be associated with one appregistry application.
If a stack is associated with multiple applications in your app or is already associated with one,
CDK will fail at deploy time.
CDK will fail at deploy time.

### Associating application with an attribute group

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CfnResourceShare } from '@aws-cdk/aws-ram';
import * as cdk from '@aws-cdk/core';
import { Names } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { StageStackAssociator } from './aspects/stack-associator';
import { IAttributeGroup } from './attribute-group';
import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common';
import { InputValidator } from './private/validation';
Expand All @@ -27,7 +28,13 @@ export interface IApplication extends cdk.IResource {
readonly applicationId: string;

/**
* Associate thisapplication with an attribute group.
* The name of the application.
* @attribute
*/
readonly applicationName?: string;

/**
* Associate this application with an attribute group.
*
* @param attributeGroup AppRegistry attribute group
*/
Expand All @@ -36,16 +43,34 @@ export interface IApplication extends cdk.IResource {
/**
* Associate this application with a CloudFormation stack.
*
* @deprecated Use `associateApplicationWithResource` instead.
* @param stack a CFN stack
*/
associateStack(stack: cdk.Stack): void;

/**
* Associate a Cloudformation statck with the application in the given stack.
*
* @param stack a CFN stack
*/
associateApplicationWithResource(stack: cdk.Stack): void;

/**
* Share this application with other IAM entities, accounts, or OUs.
*
* @param shareOptions The options for the share.
*/
shareApplication(shareOptions: ShareOptions): void;

/**
* Associate this application with all stacks under the construct node.
* NOTE: This method won't automatically register stacks under pipeline stages,
* and requires association of each pipeline stage by calling this method with stage Construct.
*
* @param construct cdk Construct
*/
associateAllStacksInScope(construct: Construct): void;

}

/**
Expand All @@ -67,6 +92,7 @@ export interface ApplicationProps {
abstract class ApplicationBase extends cdk.Resource implements IApplication {
public abstract readonly applicationArn: string;
public abstract readonly applicationId: string;
public abstract readonly applicationName?: string;
private readonly associatedAttributeGroups: Set<string> = new Set();
private readonly associatedResources: Set<string> = new Set();

Expand All @@ -89,6 +115,8 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
* Associate a stack with the application
* If the resource is already associated, it will ignore duplicate request.
* A stack can only be associated with one application.
*
* @deprecated Use `associateApplicationWithResource` instead.
*/
public associateStack(stack: cdk.Stack): void {
if (!this.associatedResources.has(stack.node.addr)) {
Expand All @@ -102,6 +130,27 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
}
}

/**
* Associate stack with the application in the stack passed as parameter.
*
* If the stack is already associated, it will ignore duplicate request.
* A stack can only be associated with one application.
*/
public associateApplicationWithResource(stack: cdk.Stack): void {
if (!this.associatedResources.has(stack.node.addr)) {
new CfnResourceAssociation(stack, 'AppRegistryAssociation', {
application: stack === cdk.Stack.of(this) ? this.applicationId : this.applicationName ?? this.applicationId,
resource: stack.stackId,
resourceType: 'CFN_STACK',
});

this.associatedResources.add(stack.node.addr);
if (stack !== cdk.Stack.of(this) && this.env.account === stack.account && !this.isStageScope(stack)) {
stack.addDependency(cdk.Stack.of(this));
}
}
}

/**
* Share an application with accounts, organizations and OUs, and IAM roles and users.
* The application will become available to end users within those principals.
Expand All @@ -120,6 +169,17 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
});
}

/**
* Associate all stacks present in construct's aspect with application.
*
* NOTE: This method won't automatically register stacks under pipeline stages,
* and requires association of each pipeline stage by calling this method with stage Construct.
*
*/
public associateAllStacksInScope(scope: Construct): void {
cdk.Aspects.of(scope).add(new StageStackAssociator(this));
}

/**
* Create a unique id
*/
Expand All @@ -139,6 +199,10 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
return shareOptions.sharePermission ?? APPLICATION_READ_ONLY_RAM_PERMISSION_ARN;
}
}

private isStageScope(stack : cdk.Stack): boolean {
return !(stack.node.scope instanceof cdk.App) && (stack.node.scope instanceof cdk.Stage);
}
}

/**
Expand All @@ -163,6 +227,7 @@ export class Application extends ApplicationBase {
class Import extends ApplicationBase {
public readonly applicationArn = applicationArn;
public readonly applicationId = applicationId!;
public readonly applicationName = undefined;

protected generateUniqueHash(resourceAddress: string): string {
return hashValues(this.applicationArn, resourceAddress);
Expand All @@ -176,6 +241,7 @@ export class Application extends ApplicationBase {

public readonly applicationArn: string;
public readonly applicationId: string;
public readonly applicationName?: string;
private readonly nodeAddress: string;

constructor(scope: Construct, id: string, props: ApplicationProps) {
Expand All @@ -190,6 +256,7 @@ export class Application extends ApplicationBase {

this.applicationArn = application.attrArn;
this.applicationId = application.attrId;
this.applicationName = props.applicationName;
this.nodeAddress = cdk.Names.nodeUniqueId(application.node);
}

Expand Down
Loading