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: Allow update of PGPSecret and PrivateKey #20

Merged
merged 9 commits into from
Dec 27, 2018
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
25 changes: 25 additions & 0 deletions build-custom-resource-handlers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
set -euo pipefail

compile="tsc --alwaysStrict
--inlineSourceMap
--lib ES2017
--module CommonJS
--moduleResolution Node
--noFallthroughCasesInSwitch
--noImplicitAny
--noImplicitReturns
--noImplicitThis
--noUnusedLocals
--noUnusedParameters
--removeComments
--strict
--target ES2017
--types node"

for handler in pgp-secret private-key certificate-signing-request
do
echo "Building CustomResource handler ${handler}"
${compile} --outDir "./custom-resource-handlers/bin/${handler}" "./custom-resource-handlers/src/${handler}.ts" ./custom-resource-handlers/src/_*.ts
mv "./custom-resource-handlers/bin/${handler}/${handler}.js" "./custom-resource-handlers/bin/${handler}/index.js"
done
157 changes: 157 additions & 0 deletions custom-resource-handlers/src/_cloud-formation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import https = require('https');
import url = require('url');
import lambda = require('./_lambda');

export type LambdaHandler = (event: Event, context: lambda.Context) => Promise<void>;
export type ResourceHandler = (event: Event, context: lambda.Context) => Promise<ResourceAttributes>;

/**
* Implements a Lambda CloudFormation custom resource handler.
*
* @param handleEvent the handler function that creates, updates and deletes the resource.
* @param refAttribute the name of the attribute holindg the Physical ID of the resource.
* @returns a handler function.
*/
export function customResourceHandler(handleEvent: ResourceHandler): LambdaHandler {
return async (event, context) => {
try {
// tslint:disable-next-line:no-console
console.log(`Input event: ${JSON.stringify(event)}`);

const attributes = await handleEvent(event, context);

// tslint:disable-next-line:no-console
console.log(`Attributes: ${JSON.stringify(attributes)}`);

await exports.sendResponse(event, Status.SUCCESS, attributes.Ref, attributes);
} catch (e) {
// tslint:disable-next-line:no-console
console.error(e);
await exports.sendResponse(event, Status.FAILED, event.PhysicalResourceId, {}, e.message);
}
};
}

/**
* General shape of custom resource attributes.
*/
export interface ResourceAttributes {
/** The physical reference to this resource instance. */
Ref: string;

/** Other attributes of the resource. */
[key: string]: string | undefined;
}

/**
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html
*/
export function sendResponse(event: Event,
status: Status,
physicalResourceId: string = event.PhysicalResourceId || event.LogicalResourceId,
data: { [name: string]: string | undefined },
reason?: string) {
const responseBody = JSON.stringify({
Data: data,
LogicalResourceId: event.LogicalResourceId,
PhysicalResourceId: physicalResourceId,
Reason: reason,
RequestId: event.RequestId,
StackId: event.StackId,
Status: status,
}, null, 2);

// tslint:disable-next-line:no-console
console.log(`Response body: ${responseBody}`);

const parsedUrl = url.parse(event.ResponseURL);
const options: https.RequestOptions = {
headers: {
'content-length': responseBody.length,
'content-type': '',
},
hostname: parsedUrl.hostname,
method: 'PUT',
path: parsedUrl.path,
port: parsedUrl.port || 443,
};

return new Promise((ok, ko) => {
// tslint:disable-next-line:no-console
console.log('Sending response...');

const req = https.request(options, resp => {
// tslint:disable-next-line:no-console
console.log(`Received HTTP ${resp.statusCode} (${resp.statusMessage})`);
if (resp.statusCode === 200) {
return ok();
}
ko(new Error(`Unexpected error sending resopnse to CloudFormation: HTTP ${resp.statusCode} (${resp.statusMessage})`));
});

req.once('error', ko);
req.write(responseBody);

req.end();
});
}

export enum Status {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
}

export enum RequestType {
CREATE = 'Create',
UPDATE = 'Update',
DELETE = 'Delete',
}

/** @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html */
export type Event = CreateEvent | UpdateEvent | DeleteEvent;

export interface CloudFormationEventBase {
readonly RequestType: RequestType;
readonly ResponseURL: string;
readonly StackId: string;
readonly RequestId: string;
readonly ResourceType: string;
readonly LogicalResourceId: string;
readonly ResourceProperties: { [name: string]: any };
}

export interface CreateEvent extends CloudFormationEventBase {
readonly RequestType: RequestType.CREATE;
readonly PhysicalResourceId: undefined;
}

export interface UpdateEvent extends CloudFormationEventBase {
readonly RequestType: RequestType.UPDATE;
readonly PhysicalResourceId: string;
readonly OldResourceProperties: { [name: string]: any };
}

export interface DeleteEvent extends CloudFormationEventBase {
readonly RequestType: RequestType.DELETE;
readonly PhysicalResourceId: string;
}

/**
* Validates that all required properties are present, and that no extraneous properties are provided.
*
* @param props the properties to be validated.
* @param validProps a mapping of valid property names to a boolean instructing whether the property is required or not.
*/
export function validateProperties(props: { [name: string]: any }, validProps: { [name: string]: boolean }) {
for (const property of Object.keys(props)) {
if (!(property in validProps)) {
throw new Error(`Unexpected property: ${property}`);
}
}
for (const property of Object.keys(validProps)) {
if (validProps[property] && !(property in props)) {
throw new Error(`Missing required property: ${property}`);
}
}
return props;
}
19 changes: 19 additions & 0 deletions custom-resource-handlers/src/_exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import childProcess = require('child_process');
import process = require('process');

export = function _exec(command: string, ...args: string[]): Promise<string> {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
return new Promise<string>((ok, ko) => {
const child = childProcess.spawn(command, args, { env: process.env, shell: false, stdio: ['ignore', 'pipe', 'inherit'] });
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
const chunks = new Array<Buffer>();

child.stdout.on('data', chunk => chunks.push(chunk));

child.once('error', ko);
child.once('exit', (code, signal) => {
if (code === 0) {
return ok(Buffer.concat(chunks).toString('utf8'));
}
ko(new Error(signal != null ? `Killed by ${signal}` : `Exited with status ${code}`));
});
});
};
90 changes: 90 additions & 0 deletions custom-resource-handlers/src/_lambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
* @see https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
*/
export interface Context {
/**
* The name of the Lambda function
*/
readonly functionName: string;

/**
* The version of the function
*/
readonly functionVersion: string;

/**
* The Amazon Resource Name (ARN) used to invoke the function. Indicates if the invoker specified a version number
* or alias.
*/
readonly invokedFunctionArn: string;

/**
* The amount of memory configured on the function.
*/
readonly memoryLimitInMB: number;

/**
* The identifier of the invocation request?
*/
readonly awsRequestId: string;

/**
* The log group for the function.
*/
readonly logGroupName: string;

/**
* The log stream for the function instance.
*/
readonly logStreamName: string;

/**
* Set to false to send the response right away when the callback executes, instead of waiting for the Node.js event
* loop to be empty. If false, any outstanding events will continue to run during the next invocation.
*/
callbackWaitsForEmptyEventLoop: boolean;

/**
* For mobile apps, information about the Amazon Cognito identity that authorized the request.
*/
identity?: {
/**
* The authenticated Amazon Cognito identity.
*/
cognitoIdentityId: string;

/**
* The Amazon Cognito identity pool that authorized the invocation.
*/
cognitoIdentityPoolId: string;
};

/**
* For mobile apps, client context provided to the Lambda invoker by the client application.
*/
clientContext?: {
client: {
installation_id: string;
app_title: string;
app_version_name: string;
app_version_code: string;
app_package_name: string;
};
env: {
platform_version: string;
platform: string;
make: string;
model: string;
locale: string;
};
/**
* Custom values set by the mobile application.
*/
Custom: { [name: string]: any };
}

/**
* Returns the number of milliseconds left before the execution times out.
*/
getRemainingTimeInMillis(): number;
}
20 changes: 20 additions & 0 deletions custom-resource-handlers/src/_rmrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs = require('fs');
import path = require('path');
import util = require('util');

const readdir = util.promisify(fs.readdir);
const rmdir = util.promisify(fs.rmdir);
const stat = util.promisify(fs.stat);
const unlink = util.promisify(fs.unlink);

export = async function _rmrf(filePath: string): Promise<void> {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
const fstat = await stat(filePath);
if (fstat.isDirectory()) {
for (const child of await readdir(filePath)) {
await _rmrf(path.join(filePath, child));
}
await rmdir(filePath);
} else {
await unlink(filePath);
}
};
Loading