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): initial L2 construct for Application #15140

Merged
merged 16 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 33 additions & 3 deletions packages/@aws-cdk/aws-servicecatalogappregistry/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AWS::ServiceCatalogAppRegistry Construct Library
# AWS ServiceCatalogAppRegistry Construct Library
<!--BEGIN STABILITY BANNER-->

---
Expand All @@ -9,12 +9,42 @@
>
> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib

![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge)

> The APIs of higher level constructs in this module are experimental and under active development.
> They are subject to non-backward compatible changes or removal in any future version. These are
> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be
> announced in the release notes. This means that while you may use them, you may need to update
> your source code when upgrading to a newer version of this package.

---

<!--END STABILITY BANNER-->

This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
[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.


```ts
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.

```ts
const application = new appreg.Application(this, 'MyFirstApplication', {
applicationName: 'MyFirstApplicationName',
description: 'description for my application', // the description is optional
});
```

An application that has been created outside of the stack can be imported into your CDK app.
Applications can be imported by their ARN via the `Application.fromApplicationArn()` API:

```ts
import servicecatalogappregistry = require('@aws-cdk/aws-servicecatalogappregistry');
const importedApplication = appreg.Application.fromApplicationArn(this, 'MyImportedApplication',
'arn:aws:servicecatalog:us-east-1:012345678910:/applications/0aqmvxvgmry0ecc4mjhwypun6i');
```
98 changes: 98 additions & 0 deletions packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as cdk from '@aws-cdk/core';
import { InputValidator } from './private/validation';
import { CfnApplication } from './servicecatalogappregistry.generated';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from 'constructs';

/**
* A Service Catalog AppRegistry Application.
*/
export interface IApplication extends cdk.IResource {
/**
* The ARN of the application.
* @attribute
*/
readonly applicationArn: string;

/**
* The ID of the application.
* @attribute
*/
readonly applicationId: string;
}

/**
* Properties for a Service Catalog AppRegistry Application
*/
export interface ApplicationProps {
/**
* Enforces a particular physical application name.
*/
readonly applicationName: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

BTW - you called this displayName in Portfolio. Any reason you went with a different name in Application?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The actual cfn term in Portfolio is DisplayName, see here. For application it is just name hence applicationName.

Copy link
Contributor

Choose a reason for hiding this comment

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

You don't want to make this consistent across the ServiceCatalog constructs that exhibit this behavior (display name + auto-generated ID)?


/**
* Description for application.
* @default - No description provided
*/
readonly description?: string;
}

abstract class ApplicationBase extends cdk.Resource implements IApplication {
public abstract readonly applicationArn: string;
public abstract readonly applicationId: string;
}
skinny85 marked this conversation as resolved.
Show resolved Hide resolved

/**
* A Service Catalog AppRegistry Application.
*/
export class Application extends ApplicationBase {
/**
* Imports an Application construct that represents an external application.
*
* @param scope The parent creating construct (usually `this`).
* @param id The construct's name.
* @param applicationArn the Amazon Resource Name of the existing AppRegistry Application
*/
public static fromApplicationArn(scope: Construct, id: string, applicationArn: string): IApplication {
const arn = cdk.Stack.of(scope).splitArn(applicationArn, cdk.ArnFormat.SLASH_RESOURCE_SLASH_RESOURCE_NAME);
const applicationId = arn.resourceName;

if (!applicationId) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed this to match our SC construct error check and messaging

throw new Error('Missing required Application ID from Application ARN: ' + applicationArn);
}

class Import extends ApplicationBase {
public readonly applicationArn = applicationArn;
public readonly applicationId = applicationId!;
}

return new Import(scope, id, {
environmentFromArn: applicationArn,
});
}

public readonly applicationArn: string;
public readonly applicationId: string;

constructor(scope: Construct, id: string, props: ApplicationProps) {
super(scope, id);

this.validateApplicationProps(props);

const application = new CfnApplication(this, 'Resource', {
name: props.applicationName,
arcrank marked this conversation as resolved.
Show resolved Hide resolved
description: props.description,
});

this.applicationArn = application.attrArn;
this.applicationId = application.attrId;
}
arcrank marked this conversation as resolved.
Show resolved Hide resolved

private validateApplicationProps(props: ApplicationProps) {
InputValidator.validateLength(this.node.path, 'application name', 1, 256, props.applicationName);
InputValidator.validateRegex(this.node.path, 'application name', /^[a-zA-Z0-9-_]+$/, props.applicationName);
InputValidator.validateLength(this.node.path, 'application description', 0, 1024, props.description);
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './application';

// AWS::ServiceCatalogAppRegistry CloudFormation Resources:
export * from './servicecatalogappregistry.generated';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as cdk from '@aws-cdk/core';

/**
* Class to validate that inputs match requirements.
*/
export class InputValidator {
/**
* Validates length is between allowed min and max lengths.
*/
public static validateLength(resourceName: string, inputName: string, minLength: number, maxLength: number, inputString?: string): void {
if (!cdk.Token.isUnresolved(inputString) && inputString !== undefined && (inputString.length < minLength || inputString.length > maxLength)) {
throw new Error(`Invalid ${inputName} for resource ${resourceName}, must have length between ${minLength} and ${maxLength}, got: '${this.truncateString(inputString, 100)}'`);
}
}

/**
* Validates a regex.
*/
public static validateRegex(resourceName: string, inputName: string, regex: RegExp, inputString?: string): void {
if (!cdk.Token.isUnresolved(inputString) && inputString !== undefined && !regex.test(inputString)) {
throw new Error(`Invalid ${inputName} for resource ${resourceName}, must match regex pattern ${regex}, got: '${this.truncateString(inputString, 100)}'`);
}
}

private static truncateString(string: string, maxLength: number): string {
if (string.length > maxLength) {
return string.substring(0, maxLength) + '[truncated]';
}
return string;
}
}
13 changes: 8 additions & 5 deletions packages/@aws-cdk/aws-servicecatalogappregistry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,26 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/assert-internal": "0.0.0",
"@types/jest": "^26.0.23",
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"cfn2ts": "0.0.0",
"pkglint": "0.0.0",
"@aws-cdk/assert-internal": "0.0.0"
"pkglint": "0.0.0"
},
"dependencies": {
"@aws-cdk/core": "0.0.0"
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"peerDependencies": {
"@aws-cdk/core": "0.0.0"
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
},
"stability": "experimental",
"maturity": "cfn-only",
"maturity": "experimental",
"awscdkio": {
"announce": false
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import '@aws-cdk/assert-internal/jest';
import * as cdk from '@aws-cdk/core';
import * as appreg from '../lib';

describe('Application', () => {
let stack: cdk.Stack;

beforeEach(() => {
stack = new cdk.Stack();
});

test('default application creation', () => {
new appreg.Application(stack, 'MyApplication', {
applicationName: 'testApplication',
});

expect(stack).toMatchTemplate({
Resources: {
MyApplication5C63EC1D: {
Type: 'AWS::ServiceCatalogAppRegistry::Application',
Properties: {
Name: 'testApplication',
},
},
},
});
}),

test('application with explicit description', () => {
const description = 'my test application description';
new appreg.Application(stack, 'MyApplication', {
applicationName: 'testApplication',
description: description,
});

expect(stack).toHaveResourceLike('AWS::ServiceCatalogAppRegistry::Application', {
Description: description,
});
}),

test('application with application tags', () => {
const application = new appreg.Application(stack, 'MyApplication', {
applicationName: 'testApplication',
});

cdk.Tags.of(application).add('key1', 'value1');
cdk.Tags.of(application).add('key2', 'value2');

expect(stack).toHaveResourceLike('AWS::ServiceCatalogAppRegistry::Application', {
Tags: {
key1: 'value1',
key2: 'value2',
},
});
}),

test('for an application imported by ARN', () => {
const application = appreg.Application.fromApplicationArn(stack, 'MyApplication',
'arn:aws:servicecatalog:us-east-1:123456789012:/applications/0aqmvxvgmry0ecc4mjhwypun6i');
expect(application.applicationId).toEqual('0aqmvxvgmry0ecc4mjhwypun6i');
}),

test('fails for application imported by ARN missing applicationId', () => {
expect(() => {
appreg.Application.fromApplicationArn(stack, 'MyApplication',
'arn:aws:servicecatalog:us-east-1:123456789012:/applications/');
}).toThrow(/Missing required Application ID from Application ARN:/);
}),

test('application created with a token description does not throw validation error and creates', () => {
const tokenDescription = new cdk.CfnParameter(stack, 'Description');

new appreg.Application(stack, 'MyApplication', {
applicationName: 'myApplication',
description: tokenDescription.valueAsString,
});

expect(stack).toHaveResourceLike('AWS::ServiceCatalogAppRegistry::Application', {
Description: {
Ref: 'Description',
},
});
}),

test('application created with a token application name does not throw validation error', () => {
const tokenApplicationName= new cdk.CfnParameter(stack, 'ApplicationName');

new appreg.Application(stack, 'MyApplication', {
applicationName: tokenApplicationName.valueAsString,
});

expect(stack).toHaveResourceLike('AWS::ServiceCatalogAppRegistry::Application', {
Name: {
Ref: 'ApplicationName',
},
});
}),

test('fails for application with description length longer than allowed', () => {
expect(() => {
new appreg.Application(stack, 'MyApplication', {
applicationName: 'testApplication',
description: 'too long description'.repeat(1000),
});
}).toThrow(/Invalid application description for resource/);
}),

test('fails for application creation with name too short', () => {
expect(() => {
new appreg.Application(stack, 'MyApplication', {
applicationName: '',
});
}).toThrow(/Invalid application name for resource/);
}),

test('fails for application with name too long', () => {
expect(() => {
new appreg.Application(stack, 'MyApplication', {
applicationName: 'testApplication'.repeat(50),
});
}).toThrow(/Invalid application name for resource/);
}),

test('fails for application with name of invalid characters', () => {
expect(() => {
new appreg.Application(stack, 'MyApplication', {
applicationName: 'My@ppl!iC@ #',
});
}).toThrow(/Invalid application name for resource/);
});
});

arcrank marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Resources": {
"TestApplication2FBC585F": {
"Type": "AWS::ServiceCatalogAppRegistry::Application",
"Properties": {
"Name": "myApplicationtest",
"Description": "my application description"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as cdk from '@aws-cdk/core';
import * as appreg from '../lib';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-application');

new appreg.Application(stack, 'TestApplication', {
applicationName: 'myApplicationtest',
description: 'my application description',
});

app.synth();