Skip to content

Commit fbccf14

Browse files
authored
Add type configuration support to the typescript cli plugin (#67)
1 parent ddcfb54 commit fbccf14

File tree

9 files changed

+334
-65
lines changed

9 files changed

+334
-65
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,5 @@ tests/**/*.d.ts
139139
# Node.js
140140
node_modules/
141141
coverage/
142+
143+
*.iml

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ repos:
77
- repo: https://github.com/ambv/black
88
rev: stable
99
hooks:
10-
- id: black
11-
# language_version: python3.6
10+
- id: black
11+
# language_version: python3.6
1212
- repo: https://github.com/pre-commit/pre-commit-hooks
1313
rev: v2.0.0
1414
hooks:

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ If you are using this package to build resource providers for CloudFormation, in
1717

1818
**Prerequisites**
1919

20-
- Python version 3.6 or above
21-
- [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
22-
- Your choice of TypeScript IDE
20+
- Python version 3.6 or above
21+
- [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
22+
- Your choice of TypeScript IDE
2323

2424
**Installation**
2525

@@ -75,6 +75,19 @@ pip3 install \
7575

7676
That ensures neither is accidentally installed from PyPI.
7777

78+
For changes to the typescript library "@amazon-web-services-cloudformation/cloudformation-cli-typescript-lib" pack up the compiled javascript:
79+
80+
```shell
81+
npm run build
82+
npm pack
83+
```
84+
85+
You can then install this in a cfn resource project using:
86+
87+
```shell
88+
npm install ../path/to/cloudformation-cli-typescript-plugin/amazon-web-services-cloudformation-cloudformation-cli-typescript-lib-1.0.1.tgz
89+
```
90+
7891
Linting and running unit tests is done via [pre-commit](https://pre-commit.com/), and so is performed automatically on commit after being installed (`pre-commit install`). The continuous integration also runs these checks. Manual options are available so you don't have to commit:
7992

8093
```shell

python/rpdk/typescript/codegen.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,24 @@ def generate(self, project):
161161

162162
models = resolve_models(project.schema)
163163

164+
if project.configuration_schema:
165+
configuration_models = resolve_models(
166+
project.configuration_schema, "TypeConfigurationModel"
167+
)
168+
else:
169+
configuration_models = {"TypeConfigurationModel": {}}
170+
171+
models.update(configuration_models)
172+
164173
path = self.package_root / "models.ts"
165174
LOG.debug("Writing file: %s", path)
166175
template = self.env.get_template("models.ts")
176+
167177
contents = template.render(
168178
lib_name=SUPPORT_LIB_NAME,
169179
type_name=project.type_name,
170180
models=models,
181+
contains_type_configuration=project.configuration_schema,
171182
primaryIdentifier=project.schema.get("primaryIdentifier", []),
172183
additionalIdentifiers=project.schema.get("additionalIdentifiers", []),
173184
)
@@ -176,6 +187,8 @@ def generate(self, project):
176187
LOG.debug("Generate complete")
177188

178189
def _pre_package(self, build_path):
190+
# Caller should own/delete this, not us.
191+
# pylint: disable=consider-using-with
179192
f = TemporaryFile("w+b")
180193

181194
# pylint: disable=unexpected-keyword-arg

python/rpdk/typescript/templates/handlers.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
ResourceHandlerRequest,
1212
SessionProxy,
1313
} from '{{lib_name}}';
14-
import { ResourceModel } from './models';
14+
import { ResourceModel, TypeConfigurationModel } from './models';
1515

1616
interface CallbackContext extends Record<string, any> {}
1717

@@ -25,14 +25,17 @@ class Resource extends BaseResource<ResourceModel> {
2525
* @param request The request object for the provisioning request passed to the implementor
2626
* @param callbackContext Custom context object to allow the passing through of additional
2727
* state or metadata between subsequent retries
28+
* @param typeConfiguration Configuration data for this resource type, in the given account
29+
* and region
2830
* @param logger Logger to proxy requests to default publishers
2931
*/
3032
@handlerEvent(Action.Create)
3133
public async create(
3234
session: Optional<SessionProxy>,
3335
request: ResourceHandlerRequest<ResourceModel>,
3436
callbackContext: CallbackContext,
35-
logger: LoggerProxy
37+
logger: LoggerProxy,
38+
typeConfiguration: TypeConfigurationModel,
3639
): Promise<ProgressEvent<ResourceModel, CallbackContext>> {
3740
const model = new ResourceModel(request.desiredResourceState);
3841
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>(model);
@@ -63,14 +66,17 @@ class Resource extends BaseResource<ResourceModel> {
6366
* @param request The request object for the provisioning request passed to the implementor
6467
* @param callbackContext Custom context object to allow the passing through of additional
6568
* state or metadata between subsequent retries
69+
* @param typeConfiguration Configuration data for this resource type, in the given account
70+
* and region
6671
* @param logger Logger to proxy requests to default publishers
6772
*/
6873
@handlerEvent(Action.Update)
6974
public async update(
7075
session: Optional<SessionProxy>,
7176
request: ResourceHandlerRequest<ResourceModel>,
7277
callbackContext: CallbackContext,
73-
logger: LoggerProxy
78+
logger: LoggerProxy,
79+
typeConfiguration: TypeConfigurationModel,
7480
): Promise<ProgressEvent<ResourceModel, CallbackContext>> {
7581
const model = new ResourceModel(request.desiredResourceState);
7682
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>(model);
@@ -88,14 +94,17 @@ class Resource extends BaseResource<ResourceModel> {
8894
* @param request The request object for the provisioning request passed to the implementor
8995
* @param callbackContext Custom context object to allow the passing through of additional
9096
* state or metadata between subsequent retries
97+
* @param typeConfiguration Configuration data for this resource type, in the given account
98+
* and region
9199
* @param logger Logger to proxy requests to default publishers
92100
*/
93101
@handlerEvent(Action.Delete)
94102
public async delete(
95103
session: Optional<SessionProxy>,
96104
request: ResourceHandlerRequest<ResourceModel>,
97105
callbackContext: CallbackContext,
98-
logger: LoggerProxy
106+
logger: LoggerProxy,
107+
typeConfiguration: TypeConfigurationModel,
99108
): Promise<ProgressEvent<ResourceModel, CallbackContext>> {
100109
const model = new ResourceModel(request.desiredResourceState);
101110
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>();
@@ -112,14 +121,17 @@ class Resource extends BaseResource<ResourceModel> {
112121
* @param request The request object for the provisioning request passed to the implementor
113122
* @param callbackContext Custom context object to allow the passing through of additional
114123
* state or metadata between subsequent retries
124+
* @param typeConfiguration Configuration data for this resource type, in the given account
125+
* and region
115126
* @param logger Logger to proxy requests to default publishers
116127
*/
117128
@handlerEvent(Action.Read)
118129
public async read(
119130
session: Optional<SessionProxy>,
120131
request: ResourceHandlerRequest<ResourceModel>,
121132
callbackContext: CallbackContext,
122-
logger: LoggerProxy
133+
logger: LoggerProxy,
134+
typeConfiguration: TypeConfigurationModel,
123135
): Promise<ProgressEvent<ResourceModel, CallbackContext>> {
124136
const model = new ResourceModel(request.desiredResourceState);
125137
// TODO: put code here
@@ -135,14 +147,17 @@ class Resource extends BaseResource<ResourceModel> {
135147
* @param request The request object for the provisioning request passed to the implementor
136148
* @param callbackContext Custom context object to allow the passing through of additional
137149
* state or metadata between subsequent retries
150+
* @param typeConfiguration Configuration data for this resource type, in the given account
151+
* and region
138152
* @param logger Logger to proxy requests to default publishers
139153
*/
140154
@handlerEvent(Action.List)
141155
public async list(
142156
session: Optional<SessionProxy>,
143157
request: ResourceHandlerRequest<ResourceModel>,
144158
callbackContext: CallbackContext,
145-
logger: LoggerProxy
159+
logger: LoggerProxy,
160+
typeConfiguration: TypeConfigurationModel,
146161
): Promise<ProgressEvent<ResourceModel, CallbackContext>> {
147162
const model = new ResourceModel(request.desiredResourceState);
148163
// TODO: put code here
@@ -154,7 +169,8 @@ class Resource extends BaseResource<ResourceModel> {
154169
}
155170
}
156171

157-
export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel);
172+
// @ts-ignore // if running against v1.0.1 or earlier of plugin the 5th argument is not known but best to ignored (runtime code may warn)
173+
export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel, null, null, TypeConfigurationModel)!;
158174

159175
// Entrypoint for production usage after registered in CloudFormation
160176
export const entrypoint = resource.entrypoint;

src/exceptions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export class NotUpdatable extends BaseHandlerException {}
2222

2323
export class InvalidRequest extends BaseHandlerException {}
2424

25+
export class InvalidTypeConfiguration extends BaseHandlerException {
26+
constructor(typeName: string, reason: string) {
27+
super(
28+
`Invalid TypeConfiguration provided for type '${typeName}'. Reason: ${reason}`,
29+
HandlerErrorCode.InvalidTypeConfiguration
30+
);
31+
}
32+
}
33+
2534
export class AccessDenied extends BaseHandlerException {}
2635

2736
export class InvalidCredentials extends BaseHandlerException {}

src/interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export enum HandlerErrorCode {
160160
ServiceInternalError = 'ServiceInternalError',
161161
NetworkFailure = 'NetworkFailure',
162162
InternalFailure = 'InternalFailure',
163+
InvalidTypeConfiguration = 'InvalidTypeConfiguration',
163164
}
164165

165166
export interface Credentials {
@@ -261,6 +262,7 @@ export class RequestData<T = Dict> extends BaseDto {
261262
@Expose() providerCredentials?: Credentials;
262263
@Expose() previousResourceProperties?: T;
263264
@Expose() previousStackTags?: Dict<string>;
265+
@Expose() typeConfiguration?: Dict<string>;
264266
}
265267

266268
export class HandlerRequest<ResourceT = Dict, CallbackT = Dict> extends BaseDto {

src/resource.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import 'reflect-metadata';
22
import { boundMethod } from 'autobind-decorator';
33

44
import { AwsTaskWorkerPool, ProgressEvent, SessionProxy } from './proxy';
5-
import { BaseHandlerException, InternalFailure, InvalidRequest } from './exceptions';
5+
import {
6+
BaseHandlerException,
7+
InternalFailure,
8+
InvalidRequest,
9+
InvalidTypeConfiguration,
10+
} from './exceptions';
611
import {
712
Action,
813
BaseModel,
@@ -40,14 +45,17 @@ const MUTATING_ACTIONS: [Action, Action, Action] = [
4045
Action.Delete,
4146
];
4247

43-
export type HandlerSignature<T extends BaseModel> = Callable<
44-
[Optional<SessionProxy>, any, Dict, LoggerProxy],
48+
export type HandlerSignature<
49+
T extends BaseModel,
50+
TypeConfiguration extends BaseModel
51+
> = Callable<
52+
[Optional<SessionProxy>, any, Dict, LoggerProxy, TypeConfiguration],
4553
Promise<ProgressEvent<T>>
4654
>;
47-
export class HandlerSignatures<T extends BaseModel> extends Map<
48-
Action,
49-
HandlerSignature<T>
50-
> {}
55+
export class HandlerSignatures<
56+
T extends BaseModel,
57+
TypeConfiguration extends BaseModel
58+
> extends Map<Action, HandlerSignature<T, TypeConfiguration>> {}
5159
class HandlerEvents extends Map<Action, string | symbol> {}
5260

5361
/**
@@ -88,7 +96,10 @@ function ensureSerialize<T extends BaseModel>(toResponse = false): MethodDecorat
8896
};
8997
}
9098

91-
export abstract class BaseResource<T extends BaseModel = BaseModel> {
99+
export abstract class BaseResource<
100+
T extends BaseModel = BaseModel,
101+
TypeConfiguration extends BaseModel = BaseModel
102+
> {
92103
protected loggerProxy: LoggerProxy;
93104
protected metricsPublisherProxy: MetricsPublisherProxy;
94105

@@ -112,10 +123,13 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
112123
public readonly typeName: string,
113124
public readonly modelTypeReference: Constructor<T>,
114125
protected readonly workerPool?: AwsTaskWorkerPool,
115-
private handlers?: HandlerSignatures<T>
126+
private handlers?: HandlerSignatures<T, TypeConfiguration>,
127+
public readonly typeConfigurationTypeReference?: Constructor<TypeConfiguration> & {
128+
deserialize: Function;
129+
}
116130
) {
117131
this.typeName = typeName || '';
118-
this.handlers = handlers || new HandlerSignatures<T>();
132+
this.handlers = handlers || new HandlerSignatures<T, TypeConfiguration>();
119133

120134
this.lambdaLogger = console;
121135
this.platformLoggerProxy = new LoggerProxy();
@@ -294,8 +308,8 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
294308

295309
public addHandler = (
296310
action: Action,
297-
f: HandlerSignature<T>
298-
): HandlerSignature<T> => {
311+
f: HandlerSignature<T, TypeConfiguration>
312+
): HandlerSignature<T, TypeConfiguration> => {
299313
this.handlers.set(action, f);
300314
return f;
301315
};
@@ -304,13 +318,16 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
304318
session: Optional<SessionProxy>,
305319
request: BaseResourceHandlerRequest<T>,
306320
action: Action,
307-
callbackContext: Dict
321+
callbackContext: Dict,
322+
typeConfiguration?: TypeConfiguration
308323
): Promise<ProgressEvent<T>> => {
309324
const actionName = action == null ? '<null>' : action.toString();
310325
if (!this.handlers.has(action)) {
311326
throw new Error(`Unknown action ${actionName}`);
312327
}
313-
const handleRequest: HandlerSignature<T> = this.handlers.get(action);
328+
const handleRequest: HandlerSignature<T, TypeConfiguration> = this.handlers.get(
329+
action
330+
);
314331
// We will make the callback context and resource states readonly
315332
// to avoid modification at a later time
316333
deepFreeze(callbackContext);
@@ -320,7 +337,8 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
320337
session,
321338
request,
322339
callbackContext,
323-
this.loggerProxy || this.platformLoggerProxy
340+
this.loggerProxy || this.platformLoggerProxy,
341+
typeConfiguration
324342
);
325343
this.log(`[${action}] handler invoked`);
326344
if (handlerResponse != null) {
@@ -473,6 +491,27 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
473491
}
474492
};
475493

494+
private castTypeConfigurationRequest = (
495+
request: HandlerRequest
496+
): TypeConfiguration => {
497+
try {
498+
if (!this.typeConfigurationTypeReference) {
499+
if (request.requestData.typeConfiguration) {
500+
throw new InternalFailure(
501+
'Type configuration supplied but running with legacy version of code which does not support type configuration.'
502+
);
503+
}
504+
return null;
505+
}
506+
return this.typeConfigurationTypeReference.deserialize(
507+
request.requestData.typeConfiguration
508+
);
509+
} catch (err) {
510+
this.log('Invalid Type Configuration');
511+
throw new InvalidTypeConfiguration(this.typeName, `${err} (${err.name}`);
512+
}
513+
};
514+
476515
// @ts-ignore
477516
public async entrypoint(
478517
eventData: any | Dict,
@@ -500,6 +539,8 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
500539
const [callerCredentials, providerCredentials] = credentials;
501540
const request = this.castResourceRequest(event);
502541

542+
const typeConfiguration = this.castTypeConfigurationRequest(event);
543+
503544
let streamName = `${event.awsAccountId}-${event.region}`;
504545
if (event.stackId && request.logicalResourceIdentifier) {
505546
streamName = `${event.stackId}/${request.logicalResourceIdentifier}`;
@@ -550,7 +591,8 @@ export abstract class BaseResource<T extends BaseModel = BaseModel> {
550591
this.callerSession,
551592
request,
552593
action,
553-
callback
594+
callback,
595+
typeConfiguration
554596
);
555597
} catch (err) {
556598
error = err;

0 commit comments

Comments
 (0)