Skip to content
This repository has been archived by the owner on Feb 1, 2023. It is now read-only.

gh-26 Remove application load-balancers and target groups when cluster deleted. #39

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
!header.js
*.d.ts
node_modules
__pycache__/
*.pyc

# CDK temporary file
cdk.context.json
Expand Down
19 changes: 18 additions & 1 deletion lib/app-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import * as cdk from "@aws-cdk/core";
import * as sam from "@aws-cdk/aws-sam";
import { GraphPlatForm } from "./platform/graph-platform";
import { GraphUninstaller } from "./platform/graph-uninstaller";
import { KaiRestApi } from "./rest-api/kai-rest-api";
import { LAMBDA_LAYER_ARN, LAMBDA_LAYER_VERSION, ADD_GRAPH_TIMEOUT, DELETE_GRAPH_TIMEOUT, DELETE_GRAPH_WORKER_BATCH_SIZE, ADD_GRAPH_WORKER_BATCH_SIZE } from "./constants";
import { LayerVersion } from "@aws-cdk/aws-lambda";
Expand Down Expand Up @@ -72,7 +73,7 @@ export class AppStack extends cdk.Stack {
batchSize: ADD_GRAPH_WORKER_BATCH_SIZE
});

new Worker(this, "DeleteGraphWorker", {
const deleteGraphWorker = new Worker(this, "DeleteGraphWorker", {
cluster: platform.eksCluster,
queue: kaiRest.deleteGraphQueue,
kubectlLayer: kubectlLambdaLayer,
Expand All @@ -81,5 +82,21 @@ export class AppStack extends cdk.Stack {
timeout: DELETE_GRAPH_TIMEOUT,
batchSize: DELETE_GRAPH_WORKER_BATCH_SIZE
});

// Graph uninstaller
new GraphUninstaller(this, "GraphUninstaller", {
getGraphsFunctionArn: kaiRest.getGraphsLambda.functionArn,
deleteGraphFunctionArn: kaiRest.deleteGraphLambda.functionArn,
kubectlLayer: kubectlLambdaLayer,
timeout: cdk.Duration.seconds(30),
dependencies: [
platform,
database,
deleteGraphWorker,
kaiRest.getGraphsLambda,
kaiRest.deleteGraphLambda,
kaiRest.deleteGraphQueue
]
});
}
}
29 changes: 29 additions & 0 deletions lib/platform/crhelper-policy-statement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2020 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { PolicyStatement } from "@aws-cdk/aws-iam";

export const crhelperPolicyStatement: PolicyStatement = new PolicyStatement({
resources: ["*"],
actions: [
"lambda:AddPermission",
"lambda:RemovePermission",
"events:PutRule",
"events:DeleteRule",
"events:PutTargets",
"events:RemoveTargets"
]
});
1 change: 1 addition & 0 deletions lib/platform/graph-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class GraphPlatForm extends cdk.Construct {

// Create cluster
this._eksCluster = new eks.Cluster(this, "EksCluster", {
version: eks.KubernetesVersion.V1_16,
kubectlEnabled: true,
vpc: vpc,
mastersRole: mastersRole,
Expand Down
26 changes: 26 additions & 0 deletions lib/platform/graph-uninstaller-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2020 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Duration, IConstruct } from "@aws-cdk/core";
import { ILayerVersion } from "@aws-cdk/aws-lambda";

export interface GraphUninstallerProps {
getGraphsFunctionArn: string;
deleteGraphFunctionArn: string;
kubectlLayer: ILayerVersion;
timeout: Duration;
dependencies: IConstruct[];
}
102 changes: 102 additions & 0 deletions lib/platform/graph-uninstaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2020 Crown Copyright
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as path from "path";
import { Construct, CustomResource } from "@aws-cdk/core";
import { GraphUninstallerProps } from "./graph-uninstaller-props";
import { Function, Runtime, AssetCode } from "@aws-cdk/aws-lambda";
import { PolicyStatement } from "@aws-cdk/aws-iam";
import { Provider } from "@aws-cdk/custom-resources";
import { crhelperPolicyStatement } from "./crhelper-policy-statement";

export class GraphUninstaller extends Construct {

constructor(scope: Construct, id: string, props: GraphUninstallerProps) {
super(scope, id);
this.createConstructs(id, props);
}

private createConstructs(id: string, props: GraphUninstallerProps) {

const uninstallGraphsLambda = new Function(this, "UninstallGraphsLambda", {
runtime: Runtime.PYTHON_3_7,
code: new AssetCode(path.join(__dirname, "lambdas")),
handler: "uninstall_graphs.handler",
layers: [ props.kubectlLayer ],
timeout: props.timeout,
environment: {
"get_graphs_function_arn": props.getGraphsFunctionArn,
"delete_graph_function_arn": props.deleteGraphFunctionArn
}
});

const lambdaInvokeGetGraphsPolicyStatement: PolicyStatement = new PolicyStatement({
resources: [
props.getGraphsFunctionArn
],
actions: [
"lambda:InvokeFunction"
]
});

const lambdaInvokeDeleteGraphPolicyStatement: PolicyStatement = new PolicyStatement({
resources: [
props.deleteGraphFunctionArn
],
actions: [
"lambda:InvokeFunction"
]
});

if (uninstallGraphsLambda.role) {
uninstallGraphsLambda.role.addToPolicy(crhelperPolicyStatement);
uninstallGraphsLambda.role.addToPolicy(lambdaInvokeGetGraphsPolicyStatement);
uninstallGraphsLambda.role.addToPolicy(lambdaInvokeDeleteGraphPolicyStatement);
}

const uninstallGraphsIsCompleteLambda = new Function(this, "UninstallGraphsIsCompleteLambda", {
runtime: Runtime.PYTHON_3_7,
code: new AssetCode(path.join(__dirname, "lambdas")),
handler: "uninstall_graphs_is_complete.handler",
layers: [ props.kubectlLayer ],
timeout: props.timeout,
environment: {
"get_graphs_function_arn": props.getGraphsFunctionArn
}
});

if (uninstallGraphsIsCompleteLambda.role) {
uninstallGraphsIsCompleteLambda.role.addToPolicy(lambdaInvokeGetGraphsPolicyStatement);
}

const uninstallGraphsCustomResourceProvider = new Provider(this, "UninstallGraphsCustomResourceProvider", {
onEventHandler: uninstallGraphsLambda,
isCompleteHandler: uninstallGraphsIsCompleteLambda
});

const uninstallGraphsCustomResource = new CustomResource(this, "UninstallGraphsCustomResource", {
serviceToken: uninstallGraphsCustomResourceProvider.serviceToken
});

/* Ensure deletion of the uninstallGraphsCustomResource occurs before the uninstallGraphsCustomResourceProvider. */
uninstallGraphsCustomResource.node.addDependency(uninstallGraphsCustomResourceProvider);

/* Ensure all the dependencies required to uninstall graphs are retained until the uninstallGraphsCustomResource has been deleted successfully. */
for (const dependency of props.dependencies) {
uninstallGraphsCustomResource.node.addDependency(dependency);
}
}
}
1 change: 1 addition & 0 deletions lib/platform/lambdas/crhelper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from crhelper.resource_helper import CfnResource, SUCCESS, FAILED
83 changes: 83 additions & 0 deletions lib/platform/lambdas/crhelper/log_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import print_function
import json
import logging


def _json_formatter(obj):
"""Formatter for unserialisable values."""
return str(obj)


class JsonFormatter(logging.Formatter):
"""AWS Lambda Logging formatter.

Formats the log message as a JSON encoded string. If the message is a
dict it will be used directly. If the message can be parsed as JSON, then
the parse d value is used in the output record.
"""

def __init__(self, **kwargs):
super(JsonFormatter, self).__init__()
self.format_dict = {
'timestamp': '%(asctime)s',
'level': '%(levelname)s',
'location': '%(name)s.%(funcName)s:%(lineno)d',
}
self.format_dict.update(kwargs)
self.default_json_formatter = kwargs.pop(
'json_default', _json_formatter)

def format(self, record):
record_dict = record.__dict__.copy()
record_dict['asctime'] = self.formatTime(record)

log_dict = {
k: v % record_dict
for k, v in self.format_dict.items()
if v
}

if isinstance(record_dict['msg'], dict):
log_dict['message'] = record_dict['msg']
else:
log_dict['message'] = record.getMessage()

# Attempt to decode the message as JSON, if so, merge it with the
# overall message for clarity.
try:
log_dict['message'] = json.loads(log_dict['message'])
except (TypeError, ValueError):
pass

if record.exc_info:
# Cache the traceback text to avoid converting it multiple times
# (it's constant anyway)
# from logging.Formatter:format
if not record.exc_text:
record.exc_text = self.formatException(record.exc_info)

if record.exc_text:
log_dict['exception'] = record.exc_text

json_record = json.dumps(log_dict, default=self.default_json_formatter)

if hasattr(json_record, 'decode'): # pragma: no cover
json_record = json_record.decode('utf-8')

return json_record


def setup(level='DEBUG', formatter_cls=JsonFormatter, boto_level=None, **kwargs):
if formatter_cls:
for handler in logging.root.handlers:
handler.setFormatter(formatter_cls(**kwargs))

logging.root.setLevel(level)

if not boto_level:
boto_level = level

logging.getLogger('boto').setLevel(boto_level)
logging.getLogger('boto3').setLevel(boto_level)
logging.getLogger('botocore').setLevel(boto_level)
logging.getLogger('urllib3').setLevel(boto_level)
Loading