Skip to content

Commit

Permalink
feat(servicecatalogappregistry): application-associator L2 Construct (#…
Browse files Browse the repository at this point in the history
…22024)

new application-associator L2 Construct : This construct is responsible for following:

* Create a new AppRegistry Application
* Associate all stacks inside a cdk app scope
* share an app registry application upon determining cross account stack. [ This only works for non environment agnostic stack]

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

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

Co-authored by: Santanu Ghosh
  • Loading branch information
rohitagg0807 authored Sep 30, 2022
1 parent 6840d8e commit a2b7a46
Show file tree
Hide file tree
Showing 21 changed files with 1,349 additions and 11 deletions.
82 changes: 77 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)
- [Application-Associator](#application-associator)
- [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,85 @@ const importedApplication = appreg.Application.fromApplicationArn(
);
```

## Application-Associator

If you want to create an Application named `MyAssociatedApplication` in account `123456789012` and region `us-east-1`
and want to associate all stacks in the `App` scope to `MyAssociatedApplication`, then use as shown in the example below:

```ts
const app = new App();
const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
applicationName: 'MyAssociatedApplication',
description: 'Testing associated application',
stackProps: {
stackName: 'MyAssociatedApplicationStack',
env: {account: '123456789012', region: 'us-east-1'},
},
});
```

If you want to re-use an existing Application with ARN: `arn:aws:servicecatalog:us-east-1:123456789012:/applications/applicationId`
and want to associate all stacks in the `App` scope to your imported application, then use as shown in the example below:

```ts
const app = new App();
const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
applicationArnValue: 'arn:aws:servicecatalog:us-east-1:123456789012:/applications/applicationId',
stackProps: {
stackName: 'MyAssociatedApplicationStack',
},
});
```

If you are using CDK Pipelines to deploy your application, the application stacks will be inside Stages, and
ApplicationAssociator will not be able to find them. Call `associateStage` on each Stage object before adding it to the
Pipeline, as shown in the example 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 application associator.
props.application.associateStage(beta);
pipeline.addStage(beta);
}
};

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

const app = new App();
const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
applicationName: 'MyPipelineAssociatedApplication',
description: 'Testing pipeline associated app',
stackProps: {
stackName: 'MyPipelineAssociatedApplicationStack',
env: {account: '123456789012', region: 'us-east-1'},
},
});

const cdkPipeline = new ApplicationPipelineStack(app, 'CDKApplicationPipelineStack', {
application: associatedApp,
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 +176,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
@@ -0,0 +1,100 @@
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { IApplication, Application } from './application';
import { CheckedStageStackAssociator } from './aspects/stack-associator';

/**
* Properties for a Service Catalog AppRegistry AutoApplication
*/
export interface ApplicationAssociatorProps {
/**
* Enforces a particular physical application name.
*
* @default - No name.
*/
readonly applicationName?: string;

/**
* Enforces a particular application arn.
*
* @default - No application arn.
*/
readonly applicationArnValue?: string;

/**
* Application description.
*
* @default - No description.
*/
readonly description?: string;

/**
* Stack properties.
*
*/
readonly stackProps: cdk.StackProps;
}

/**
* An AppRegistry 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.
* This 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.
*/
export class ApplicationAssociator extends Construct {
/**
* Created or imported application.
*/
private readonly application: IApplication;
private readonly associatedStages: Set<cdk.Stage> = new Set();

constructor(scope: cdk.App, id: string, props: ApplicationAssociatorProps) {
super(scope, id);

const applicationStack = new cdk.Stack(scope, 'ApplicationAssociatorStack', props.stackProps);

if (!!props.applicationArnValue) {
this.application = Application.fromApplicationArn(applicationStack, 'ImportedApplication', props.applicationArnValue);
} else if (!!props.applicationName) {
this.application = new Application(applicationStack, 'DefaultCdkApplication', {
applicationName: props.applicationName,
description: props.description,
});
} else {
throw new Error('Please provide either ARN or application name.');
}

cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this));
}

/**
* Associate this application with the given stage.
*
*/
public associateStage(stage: cdk.Stage): cdk.Stage {
this.associatedStages.add(stage);
cdk.Aspects.of(stage).add(new CheckedStageStackAssociator(this));
return stage;
}

/**
* Validates if a stage is already associated to the application.
*
*/
public isStageAssociated(stage: cdk.Stage): boolean {
return this.associatedStages.has(stage);
}

/**
* Get the AppRegistry application.
*
*/
get appRegistryApplication() {
return this.application;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ 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 { isAccountUnresolved } from './private/utils';
import { InputValidator } from './private/validation';
import { CfnApplication, CfnAttributeGroupAssociation, CfnResourceAssociation } from './servicecatalogappregistry.generated';

Expand All @@ -27,7 +29,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 +44,34 @@ export interface IApplication extends cdk.IResource {
/**
* Associate this application with a CloudFormation stack.
*
* @deprecated Use `associateApplicationWithStack` 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
*/
associateApplicationWithStack(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 +93,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 +116,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 `associateApplicationWithStack` instead.
*/
public associateStack(stack: cdk.Stack): void {
if (!this.associatedResources.has(stack.node.addr)) {
Expand All @@ -102,6 +131,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 associateApplicationWithStack(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.isSameAccount(stack) && !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 +170,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 +200,21 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
return shareOptions.sharePermission ?? APPLICATION_READ_ONLY_RAM_PERMISSION_ARN;
}
}

/**
* Checks whether a stack is defined in a Stage or not.
*/
private isStageScope(stack : cdk.Stack): boolean {
return !(stack.node.scope instanceof cdk.App) && (stack.node.scope instanceof cdk.Stage);
}

/**
* Verifies if application and the visited node is deployed in different account.
*/
private isSameAccount(stack: cdk.Stack): boolean {
return isAccountUnresolved(this.env.account, stack.account) || this.env.account === stack.account;
}

}

/**
Expand All @@ -163,6 +239,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 +253,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 +268,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

0 comments on commit a2b7a46

Please sign in to comment.