Skip to content

Commit

Permalink
Merge pull request gchq#12 from jpelbertrios/gchqgh-7-ui-develop
Browse files Browse the repository at this point in the history
Gh 07 ui develop
  • Loading branch information
macenturalxl1 committed Sep 4, 2020
2 parents 534a737 + bb79896 commit c84dcce
Show file tree
Hide file tree
Showing 22 changed files with 1,053 additions and 515 deletions.
20 changes: 11 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
.classpath.txt
target
.classpath
.project
.idea
.settings
.vscode
*.iml
node_modules/
*.js
!jest.config.js
!**/lambdas/*.js
!header.js
*.d.ts
node_modules
__pycache__/
*.pyc

# CDK temporary file
cdk.context.json

# CDK asset staging directory
.cdk.staging
Expand Down
26 changes: 23 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ dist: bionic
node_js:
- node

script:
- npm run lint
- npm run test
jobs:
fast_finish: true
include:
- name: Infrastructure
before_install:
- cd infrastructure/

install:
- npm install

script:
- npm run lint
- npm run test

- name: UI
before_install:
- cd ui/

install:
- npm install

script:
- npm run test
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
43 changes: 41 additions & 2 deletions infrastructure/lib/app-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { GraphDatabase } from "./database/graph-database";
import { Worker } from "./workers/worker";
import { KaiUserPool } from "./authentication/user-pool";
import { GraphDatabaseProps } from "./database/graph-database-props";
import { PolicyStatement } from "@aws-cdk/aws-iam";

// The main stack for Kai
export class AppStack extends cdk.Stack {
Expand Down Expand Up @@ -62,6 +63,21 @@ export class AppStack extends cdk.Stack {
const layerVersionArn = samApp.getAtt("Outputs.LayerVersionArn").toString();
const kubectlLambdaLayer = LayerVersion.fromLayerVersionArn(this, "KubectlLambdaLayer", layerVersionArn);

// Describe EKS cluster policy statement
const describeClusterPolicyStatement = new PolicyStatement({
actions: [ "eks:DescribeCluster" ],
resources: [ platform.eksCluster.clusterArn ]
});

// Manage EBS Volumes policy statement
const manageVolumesPolicyStatement = new PolicyStatement({
resources: ["*"],
actions: [
"ec2:DescribeVolumes",
"ec2:DeleteVolume"
]
});

// Workers
new Worker(this, "AddGraphWorker", {
cluster: platform.eksCluster,
Expand All @@ -70,7 +86,10 @@ export class AppStack extends cdk.Stack {
graphTable: database.table,
handler: "add_graph.handler",
timeout: ADD_GRAPH_TIMEOUT,
batchSize: ADD_GRAPH_WORKER_BATCH_SIZE
batchSize: ADD_GRAPH_WORKER_BATCH_SIZE,
policyStatements: [
describeClusterPolicyStatement
]
});

const deleteGraphWorker = new Worker(this, "DeleteGraphWorker", {
Expand All @@ -80,7 +99,27 @@ export class AppStack extends cdk.Stack {
graphTable: database.table,
handler: "delete_graph.handler",
timeout: DELETE_GRAPH_TIMEOUT,
batchSize: DELETE_GRAPH_WORKER_BATCH_SIZE
batchSize: DELETE_GRAPH_WORKER_BATCH_SIZE,
policyStatements: [
describeClusterPolicyStatement,
manageVolumesPolicyStatement
]
});

// 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
]
});

// Graph uninstaller
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ const TIMEOUT_FOR_ADDING_GRAPH_IN_MINUTES = 5; // how long it should take for on
const TIMEOUT_FOR_DELETING_GRAPH_IN_MINUTES = 2; // how long it should take for one graph to be deleted

export const DELETE_GRAPH_TIMEOUT = Duration.minutes(TIMEOUT_FOR_DELETING_GRAPH_IN_MINUTES * DELETE_GRAPH_WORKER_BATCH_SIZE);
export const ADD_GRAPH_TIMEOUT = Duration.minutes(TIMEOUT_FOR_ADDING_GRAPH_IN_MINUTES * ADD_GRAPH_WORKER_BATCH_SIZE);
export const ADD_GRAPH_TIMEOUT = Duration.minutes(TIMEOUT_FOR_ADDING_GRAPH_IN_MINUTES * ADD_GRAPH_WORKER_BATCH_SIZE);
2 changes: 1 addition & 1 deletion infrastructure/lib/platform/graph-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class GraphPlatForm extends cdk.Construct {

// Create cluster
this._eksCluster = new eks.Cluster(this, "EksCluster", {
version: eks.KubernetesVersion.V1_16,
version: eks.KubernetesVersion.V1_17,
kubectlEnabled: true,
vpc: vpc,
mastersRole: mastersRole,
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/lib/platform/lambdas/uninstall_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ def delete(event, context):
InvocationType = "RequestResponse",
Payload = json.dumps({
"pathParameters": {
"graphId": graph["graphId"]
"graphName": graph["graphName"]
}
})
)
responsePayloadJson = json.loads(response["Payload"].read().decode("utf-8"))
logger.info("Received responsePayloadJson: {}".format(responsePayloadJson))
if responsePayloadJson["statusCode"] != 202:
logger.error("Unable to delete graph: {}, received status code: {}, message: {}".format(
graph["graphId"],
graph["graphName"],
responsePayloadJson["statusCode"],
responsePayloadJson["body"]
)
Expand Down
12 changes: 6 additions & 6 deletions infrastructure/lib/rest-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ curl -H "Authorization: <IdToken>" https://<restapi-id>.execute-api.<aws-region>
The Graphs resource enables creation, deletion and retrieval of Graphs managed by Kai.

#### GET /graphs
Retrieves all graphs objects from the backend database. At present this only includes the graphId and its current state but this is likely to change as the project grows.
Retrieves all graphs objects from the backend database. At present this only includes the graphName and its current state but this is likely to change as the project grows.

A graph can be in different states. At present these states can be:
* DEPLOYMENT_QUEUED
Expand All @@ -77,7 +77,7 @@ Example response:
]
```

#### GET /graphs/{graphId}
#### GET /graphs/{graphName}
Retrieves a single graph from the backend database. If the Graph Id is not found, a 404 response is sent. If the requesting user is not a configured administrator of the graph a 403 response is returned.

Example response:
Expand All @@ -89,12 +89,12 @@ Example response:
```

#### POST /graphs
Creates and deploys a new graph. This endpoint is asynchronous meaning it will return before deploying a graph which takes around 5 minutes. At present, you need to provide a Gaffer schema which is split into two parts: elements and types, as well as a graphId which must be unique. This endpoint will respond with a simple 201 return code. If the user requests a graph which is already created, A 400 response will be sent, along with an error message. There is a constraint in gaffer-docker that graph ids have to be lowercase alphanumerics. We hope to address this in a bugfix to allow uppercase alphanumerics too. By default only the creating user has administration access to the graph through the REST API. If you wish to specify additional users with administration privileges they can be listed in an optional "administrators" property. If an attempt is made to configure users who are not members of the Cognito User Pool a 400 response will be returned.
Creates and deploys a new graph. This endpoint is asynchronous meaning it will return before deploying a graph which takes around 5 minutes. At present, you need to provide a Gaffer schema which is split into two parts: elements and types, as well as a graphName which must be unique. This endpoint will respond with a simple 201 return code. If the user requests a graph which is already created, A 400 response will be sent, along with an error message. There is a constraint in gaffer-docker that graph names have to be lowercase alphanumerics. We hope to address this in a bugfix to allow uppercase alphanumerics too. By default only the creating user has administration access to the graph through the REST API. If you wish to specify additional users with administration privileges they can be listed in an optional "administrators" property. If an attempt is made to configure users who are not members of the Cognito User Pool a 400 response will be returned.

Example request body:
```json
{
"graphId": "basic",
"graphName": "basic",
"administrators": [],
"schema": {
"elements": {
Expand Down Expand Up @@ -135,5 +135,5 @@ Example request body:
}
```

#### DELETE /graphs/{graphId}
Deletes a graph deployment from the platform. This endpoint is asynchronous meaning that it will respond before the graph deployment is removed. Once the graph deployment is removed, the graph will be removed from the backend database. If the requested graphId is not present or is not in the backend database at the start, a 400 error is returned. If the user is not an administrator a 403 response is returned. Otherwise a 202 status code is returned.
#### DELETE /graphs/{graphName}
Deletes a graph deployment from the platform. This endpoint is asynchronous meaning that it will respond before the graph deployment is removed. Once the graph deployment is removed, the graph will be removed from the backend database. If the requested graphName is not present or is not in the backend database at the start, a 400 error is returned. If the user is not an administrator a 403 response is returned. Otherwise a 202 status code is returned.
1 change: 0 additions & 1 deletion infrastructure/lib/rest-api/kai-rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export class KaiRestApi extends cdk.Construct {

props.graphTable.grantReadData(this._getGraphsLambda);
// Both GET and GET all are served by the same lambda

const getGraphIntegration = new api.LambdaIntegration(this._getGraphsLambda);
graphsResource.addMethod("GET", getGraphIntegration, methodOptions);
graph.addMethod("GET", getGraphIntegration, methodOptions);
Expand Down
143 changes: 98 additions & 45 deletions infrastructure/lib/rest-api/lambdas/get_graph_request.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,100 @@
import os
import boto3
from graph import Graph
import json
from user import User

graph = Graph()
user = User()

def handler(event, context):
"""
Main entrypoint for the HTTP GET lambda functions. This function
serves both GET handlers so returns all graphs if no graphName
is specified in the path parameters
"""
path_params = event["pathParameters"]
return_all = False
graph_name = None
if path_params is None or path_params["graphName"] is None:
return_all = True
else:
graph_name = path_params["graphName"]

requesting_user = user.get_requesting_cognito_user(event)

if return_all:
return {
"statusCode": 200,
"body": json.dumps(graph.get_all_graphs(requesting_user))
}
else:
if not user.is_authorized(requesting_user, graph_name):
return {
"statusCode": 403,
"body": "User: {} is not authorized to retrieve graph: {}".format(requesting_user, graph_name)
}
import subprocess
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

standard_kubeconfig="/tmp/kubeconfig"


class CommandHelper:
@staticmethod
def run_command(cmd, release_name):
succeeded=False
try:
return {
"statusCode": 200,
"body": json.dumps(graph.get_graph(graph.format_graph_name(graph_name)))
}
except Exception as e:
return {
"statusCode": 404,
"body": graph_name + " was not found"
}
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True, cwd="/tmp")
succeeded=True
except subprocess.CalledProcessError as err:
logger.error("Error during excution of command: %s against release name: %s", cmd, release_name)
logger.error(err.output)

return succeeded


class KubeConfigurator:
def run_once(f):
def wrapper(*args, **kwargs):
if not wrapper.has_run:
wrapper.has_run = True
return f(*args, **kwargs)
wrapper.has_run = False
return wrapper

@run_once
def update_kube_config(self, cluster_name, kubeconfig=standard_kubeconfig):
logger.info("Configuring kubectl for cluster: %s", cluster_name);
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--kubeconfig', kubeconfig
])


class HelmClient:
__HELM_CMD="helm"

def __init__(self, cluster_name, kubeconfig=standard_kubeconfig):
KubeConfigurator().update_kube_config(cluster_name=cluster_name, kubeconfig=kubeconfig)
self.kubeconfig = kubeconfig

def __run(self, instruction, release_name, values=None, chart=None, repo=None):
"""
Runs a Helm command and returns True if it succeeds and False if it fails
"""
cmd = [ self.__HELM_CMD, instruction, release_name ]
if chart is not None:
cmd.append(chart)
if repo is not None:
cmd.extend(["--repo", repo])
if values is not None:
cmd.extend(["--values", values])
cmd.extend(["--kubeconfig", self.kubeconfig])

return CommandHelper.run_command(cmd, release_name)

def install_chart(self, release_name, values=None, chart="gaffer", repo="https://gchq.github.io/gaffer-docker"):
"""
Installs a Helm chart and returns True if it Succeeds and False if it fails
"""
return self.__run(instruction="install", release_name=release_name, values=values, chart=chart, repo=repo)

def uninstall_chart(self, release_name):
"""
Uninstalls a Helm release and returns True if it Succeeds and False if it fails
"""
return self.__run(instruction="uninstall", release_name=release_name)


class KubernetesClient:
__KUBECTL_CMD="kubectl"

def __init__(self, cluster_name, kubeconfig=standard_kubeconfig):
KubeConfigurator().update_kube_config(cluster_name=cluster_name, kubeconfig=kubeconfig)
self.kubeconfig = kubeconfig

def delete_volumes(self, release_name):
"""
Deletes the Persistent Volume Claims associated to a release_name
"""
# HDFS Datanodes & Namenode
self.__delete_volumes(release_name=release_name, selectors=["app.kubernetes.io/instance={}".format(release_name)])

# Zookeeper
self.__delete_volumes(release_name=release_name, selectors=["release={}".format(release_name)])

def __delete_volumes(self, release_name, selectors):
cmd = [ self.__KUBECTL_CMD, "delete", "pvc", "--kubeconfig", self.kubeconfig ]
for selector in selectors:
cmd.append("--selector")
cmd.append(selector)

CommandHelper.run_command(cmd, release_name)
7 changes: 3 additions & 4 deletions infrastructure/lib/rest-api/lambdas/graph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ def get_all_graphs(self, requesting_user):
return list(filter(lambda graph: requesting_user in graph["administrators"], graphs))


def get_graph(self, release_name):
def get_graph(self, graph_name):
"""
Gets a specific graph from Dynamodb table
"""
response = self.table.get_item(
Key={
"releaseName": release_name
"releaseName": format_graph_name(graph_name)
}
)
if "Item" in response:
Expand Down Expand Up @@ -60,5 +60,4 @@ def create_graph(self, release_name, graph_name, status, administrators):
"administrators": administrators
},
ConditionExpression=boto3.dynamodb.conditions.Attr("releaseName").not_exists()
)

)
4 changes: 2 additions & 2 deletions infrastructure/lib/rest-api/lambdas/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ def get_requesting_cognito_user(self, request):
return None
return request["requestContext"]["authorizer"]["claims"]["cognito:username"]

def is_authorized(self, user, graphId):
def is_authorized(self, user, graphName):
# If Authenticated through AWS account treat as admin for all graphs
if (user is None):
return True
# Otherwise check the list of administrators configured on the graph
try:
graph_record = self.graph.get_graph(graphId)
graph_record = self.graph.get_graph(graphName)
return user in graph_record["administrators"]
except Exception as e:
return False
Loading

0 comments on commit c84dcce

Please sign in to comment.