From 4fcf6c38d9426cef147c9b9d6dfc599ce9d33d63 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 2 Dec 2018 18:18:17 -0600 Subject: [PATCH 01/16] Add specific task mutations to the api --- services/api/src/resolvers.js | 6 + .../api/src/resources/environment/helpers.js | 15 ++ .../src/resources/environment/validators.js | 72 ++++++ services/api/src/resources/task/helpers.js | 111 ++++++++ services/api/src/resources/task/resolvers.js | 241 +++++++++--------- services/api/src/resources/task/sql.js | 6 - services/api/src/typeDefs.js | 9 + 7 files changed, 334 insertions(+), 126 deletions(-) create mode 100644 services/api/src/resources/environment/helpers.js create mode 100644 services/api/src/resources/environment/validators.js create mode 100644 services/api/src/resources/task/helpers.js diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 27ca69273e..b99d623cf4 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -27,6 +27,9 @@ const { addTask, deleteTask, updateTask, + taskDrushArchiveDump, + taskDrushSqlSync, + taskDrushRsyncFiles, } = require('./resources/task/resolvers'); const { @@ -257,6 +260,9 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { addEnvVariable, deleteEnvVariable, addTask, + taskDrushArchiveDump, + taskDrushSqlSync, + taskDrushRsyncFiles, deleteTask, updateTask, setEnvironmentServices, diff --git a/services/api/src/resources/environment/helpers.js b/services/api/src/resources/environment/helpers.js new file mode 100644 index 0000000000..60e66911f3 --- /dev/null +++ b/services/api/src/resources/environment/helpers.js @@ -0,0 +1,15 @@ +// @flow + +const R = require('ramda'); +const sqlClient = require('../../clients/sqlClient'); +const { query } = require('../../util/db'); +const Sql = require('./sql'); + +const Helpers = { + getEnvironmentById: async (environmentID /* : number */) => { + const rows = await query(sqlClient, Sql.selectEnvironmentById(environmentID)); + return R.prop(0, rows); + }, +}; + +module.exports = Helpers; diff --git a/services/api/src/resources/environment/validators.js b/services/api/src/resources/environment/validators.js new file mode 100644 index 0000000000..8d2d6c6a50 --- /dev/null +++ b/services/api/src/resources/environment/validators.js @@ -0,0 +1,72 @@ +// @flow + +const R = require('ramda'); +const { prepare, query } = require('../../util/db'); +const sqlClient = require('../../clients/sqlClient'); +const Sql = require('./sql'); + +const Validators = { + environmentExists: async (environmentId /* : number */) => { + const env = await query( + sqlClient, + Sql.selectEnvironmentById(environmentId), + ); + + if (R.has('info', env)) { + throw new Error(`Environment ID ${environmentId} doesn't exist.`); + } + }, + environmentsHaveSameProject: async (environmentIds /* : number[] */) => { + const preparedQuery = prepare( + sqlClient, + ` + SELECT DISTINCT project FROM environment WHERE id in (?) + `, + ); + + const rows = await query(sqlClient, preparedQuery([environmentIds])); + const projectIds = R.pluck('project', rows); + + if (R.length(R.uniq(projectIds)) > 1) { + throw new Error( + `Environments ${environmentIds.join( + ',', + )} do not belong to the same project.`, + ); + } + }, + environmentHasService: async (environmentId /* : number */, service /* : string */) => { + const rows = await query(sqlClient, Sql.selectServicesByEnvironmentId(environmentId)); + + if (!R.contains(service, R.pluck('name', rows))) { + throw new Error(`Environment ${environmentId} has no service ${service}`); + } + }, + userAccessEnvironment: async ( + credentials /* : Object */, + environmentId /* : number */, + ) => { + const { + role, + permissions: { customers, projects }, + } = credentials; + + if (role === 'admin') { + return; + } + + const rows = await query( + sqlClient, + Sql.selectPermsForEnvironment(environmentId), + ); + + if ( + !R.contains(R.path(['0', 'pid'], rows), projects) && + !R.contains(R.path(['0', 'cid'], rows), customers) + ) { + throw new Error(`No access to environment ${environmentId}.`); + } + }, +}; + +module.exports = Validators; diff --git a/services/api/src/resources/task/helpers.js b/services/api/src/resources/task/helpers.js new file mode 100644 index 0000000000..42e2f320df --- /dev/null +++ b/services/api/src/resources/task/helpers.js @@ -0,0 +1,111 @@ +// @flow + +const R = require('ramda'); +const { sendToLagoonLogs } = require('@lagoon/commons/src/logs'); +const { createTaskTask } = require('@lagoon/commons/src/tasks'); +const { query } = require('../../util/db'); +const sqlClient = require('../../clients/sqlClient'); +const esClient = require('../../clients/esClient'); +const Sql = require('./sql'); +const projectSql = require('../project/sql'); +const environmentSql = require('../environment/sql'); + +const Helpers = { + addTask: async ({ + id, + name, + status, + created, + started, + completed, + environment, + service, + command, + remoteId, + execute, + } /* : { id?: number, name: string, status?: string, created?: string, started?: string, completed?: string, environment: number, service: string, command: string, remoteId?: string, execute: boolean } */) => { + const { + info: { insertId }, + } = await query( + sqlClient, + Sql.insertTask({ + id, + name, + status, + created, + started, + completed, + environment, + service, + command, + remoteId, + }), + ); + + let rows = await query(sqlClient, Sql.selectTask(insertId)); + const taskData = R.prop(0, rows); + + // Allow creating task data w/o executing the task + if (execute === false) { + return taskData; + } + + rows = await query(sqlClient, environmentSql.selectEnvironmentById(taskData.environment)); + const environmentData = R.prop(0, rows); + + rows = await query(sqlClient, projectSql.selectProject(environmentData.project)); + const projectData = R.prop(0, rows); + + try { + await createTaskTask({ task: taskData, project: projectData, environment: environmentData }); + } catch (error) { + sendToLagoonLogs( + 'error', + projectData.name, + '', + 'api:addTask', + { taskId: taskData.id }, + `*[${projectData.name}]* Task not initiated, reason: ${error}`, + ); + } + + return taskData; + }, + injectLogs: async (task /* : Object */) => { + if (!task.remoteId) { + return { + ...task, + logs: null, + }; + } + + const result = await esClient.search({ + index: 'lagoon-logs-*', + sort: '@timestamp:desc', + body: { + query: { + bool: { + must: [ + { match_phrase: { 'meta.remoteId': task.remoteId } }, + { match_phrase: { 'meta.jobStatus': task.status } }, + ], + }, + }, + }, + }); + + if (!result.hits.total) { + return { + ...task, + logs: null, + }; + } + + return { + ...task, + logs: R.path(['hits', 'hits', 0, '_source', 'message'], result), + }; + }, +}; + +module.exports = Helpers; diff --git a/services/api/src/resources/task/resolvers.js b/services/api/src/resources/task/resolvers.js index 4d4738de9a..13ee10f8c5 100644 --- a/services/api/src/resources/task/resolvers.js +++ b/services/api/src/resources/task/resolvers.js @@ -1,9 +1,6 @@ // @flow const R = require('ramda'); -const { sendToLagoonLogs } = require('@lagoon/commons/src/logs'); -const { createTaskTask } = require('@lagoon/commons/src/tasks'); -const esClient = require('../../clients/esClient'); const sqlClient = require('../../clients/sqlClient'); const { knex, @@ -14,8 +11,9 @@ const { isPatchEmpty, } = require('../../util/db'); const Sql = require('./sql'); -const projectSql = require('../project/sql'); -const environmentSql = require('../environment/sql'); +const Helpers = require('./helpers'); +const environmentHelpers = require('../environment/helpers'); +const envValidators = require('../environment/validators'); /* :: @@ -30,42 +28,6 @@ const taskStatusTypeToString = R.cond([ [R.T, R.identity], ]); -const injectLogs = async task => { - if (!task.remoteId) { - return { - ...task, - logs: null, - }; - } - - const result = await esClient.search({ - index: 'lagoon-logs-*', - sort: '@timestamp:desc', - body: { - query: { - bool: { - must: [ - { match_phrase: { 'meta.remoteId': task.remoteId } }, - { match_phrase: { 'meta.jobStatus': task.status } }, - ], - }, - }, - }, - }); - - if (!result.hits.total) { - return { - ...task, - logs: null, - }; - } - - return { - ...task, - logs: R.path(['hits', 'hits', 0, '_source', 'message'], result), - }; -}; - const getTasksByEnvironmentId = async ( { id: eid }, args, @@ -93,7 +55,7 @@ const getTasksByEnvironmentId = async ( const rows = await query(sqlClient, prep({ eid })); - return rows.map(row => injectLogs(row)); + return rows.map(row => Helpers.injectLogs(row)); }; const getTaskByRemoteId = async ( @@ -128,7 +90,7 @@ const getTaskByRemoteId = async ( } } - return injectLogs(task); + return Helpers.injectLogs(task); }; const addTask = async ( @@ -145,78 +107,37 @@ const addTask = async ( service, command, remoteId, - execute, + execute: executeRequest, }, }, { + credentials, credentials: { role, - permissions: { customers, projects }, }, }, ) => { const status = taskStatusTypeToString(unformattedStatus); + const execute = role === 'admin' ? executeRequest : true; + + await envValidators.environmentExists(environment); + await envValidators.userAccessEnvironment(credentials, environment); + + const taskData = await Helpers.addTask({ + id, + name, + status, + created, + started, + completed, + environment, + service, + command, + remoteId, + execute, + }); - if (role !== 'admin') { - const rows = await query( - sqlClient, - Sql.selectPermsForEnvironment(environment), - ); - - if ( - !R.contains(R.path(['0', 'pid'], rows), projects) && - !R.contains(R.path(['0', 'cid'], rows), customers) - ) { - throw new Error('Unauthorized.'); - } - } - - const { - info: { insertId }, - } = await query( - sqlClient, - Sql.insertTask({ - id, - name, - status, - created, - started, - completed, - environment, - service, - command, - remoteId, - }), - ); - - let rows = await query(sqlClient, Sql.selectTask(insertId)); - const taskData = R.prop(0, rows); - - // Allow creating task data w/o executing the task - if (role === 'admin' && execute === false) { - return injectLogs(taskData); - } - - rows = await query(sqlClient, environmentSql.selectEnvironmentById(taskData.environment)); - const environmentData = R.prop(0, rows); - - rows = await query(sqlClient, projectSql.selectProject(environmentData.project)); - const projectData = R.prop(0, rows); - - try { - await createTaskTask({ task: taskData, project: projectData, environment: environmentData }); - } catch (error) { - sendToLagoonLogs( - 'error', - projectData.name, - '', - 'api:addTask', - { taskId: taskData.id }, - `*[${projectData.name}]* Task not initiated, reason: ${error}`, - ); - } - - return injectLogs(taskData); + return Helpers.injectLogs(taskData); }; const deleteTask = async ( @@ -265,6 +186,7 @@ const updateTask = async ( }, }, { + credentials, credentials: { role, permissions: { customers, projects }, @@ -273,8 +195,8 @@ const updateTask = async ( ) => { const status = taskStatusTypeToString(unformattedStatus); + // Check access to modify task as it currently stands if (role !== 'admin') { - // Check access to modify task as it currently stands const rowsCurrent = await query(sqlClient, Sql.selectPermsForTask(id)); if ( @@ -283,21 +205,11 @@ const updateTask = async ( ) { throw new Error('Unauthorized.'); } - - // Check access to modify task as it will be updated - const rowsNew = await query( - sqlClient, - Sql.selectPermsForEnvironment(environment), - ); - - if ( - !R.contains(R.path(['0', 'pid'], rowsNew), projects) && - !R.contains(R.path(['0', 'cid'], rowsNew), customers) - ) { - throw new Error('Unauthorized.'); - } } + // Check access to modify task as it will be updated + await envValidators.userAccessEnvironment(credentials, environment); + if (isPatchEmpty({ patch })) { throw new Error('Input patch requires at least 1 attribute'); } @@ -322,7 +234,93 @@ const updateTask = async ( const rows = await query(sqlClient, Sql.selectTask(id)); - return injectLogs(R.prop(0, rows)); + return Helpers.injectLogs(R.prop(0, rows)); +}; + +const taskDrushArchiveDump = async ( + root, + { + environment, + }, + { + credentials, + }, +) => { + await envValidators.environmentExists(environment); + await envValidators.userAccessEnvironment(credentials, environment); + await envValidators.environmentHasService(environment, 'cli'); + + const taskData = await Helpers.addTask({ + name: 'Drush archive-dump', + environment, + service: 'cli', + command: 'drush archive-dump', + execute: true, + }); + + return Helpers.injectLogs(taskData); +}; + +const taskDrushSqlSync = async ( + root, + { + sourceEnvironment: sourceEnvironmentId, + destinationEnvironment: destinationEnvironmentId, + }, + { + credentials, + }, +) => { + await envValidators.environmentExists(sourceEnvironmentId); + await envValidators.environmentExists(destinationEnvironmentId); + await envValidators.environmentsHaveSameProject([sourceEnvironmentId, destinationEnvironmentId]); + await envValidators.userAccessEnvironment(credentials, sourceEnvironmentId); + await envValidators.userAccessEnvironment(credentials, destinationEnvironmentId); + await envValidators.environmentHasService(sourceEnvironmentId, 'cli'); + + const sourceEnvironment = await environmentHelpers.getEnvironmentById(sourceEnvironmentId); + const destinationEnvironment = await environmentHelpers.getEnvironmentById(destinationEnvironmentId); + + const taskData = await Helpers.addTask({ + name: `Sync DB ${sourceEnvironment.name} -> ${destinationEnvironment.name}`, + environment: destinationEnvironmentId, + service: 'cli', + command: `drush -y sql-sync @${sourceEnvironment.name} @self`, + execute: true, + }); + + return Helpers.injectLogs(taskData); +}; + +const taskDrushRsyncFiles = async ( + root, + { + sourceEnvironment: sourceEnvironmentId, + destinationEnvironment: destinationEnvironmentId, + }, + { + credentials, + }, +) => { + await envValidators.environmentExists(sourceEnvironmentId); + await envValidators.environmentExists(destinationEnvironmentId); + await envValidators.environmentsHaveSameProject([sourceEnvironmentId, destinationEnvironmentId]); + await envValidators.userAccessEnvironment(credentials, sourceEnvironmentId); + await envValidators.userAccessEnvironment(credentials, destinationEnvironmentId); + await envValidators.environmentHasService(sourceEnvironmentId, 'cli'); + + const sourceEnvironment = await environmentHelpers.getEnvironmentById(sourceEnvironmentId); + const destinationEnvironment = await environmentHelpers.getEnvironmentById(destinationEnvironmentId); + + const taskData = await Helpers.addTask({ + name: `Sync files ${sourceEnvironment.name} -> ${destinationEnvironment.name}`, + environment: destinationEnvironmentId, + service: 'cli', + command: `drush -y rsync @${sourceEnvironment.name}:%files @self:%files`, + execute: true, + }); + + return Helpers.injectLogs(taskData); }; const Resolvers /* : ResolversObj */ = { @@ -331,6 +329,9 @@ const Resolvers /* : ResolversObj */ = { addTask, deleteTask, updateTask, + taskDrushArchiveDump, + taskDrushSqlSync, + taskDrushRsyncFiles, }; module.exports = Resolvers; diff --git a/services/api/src/resources/task/sql.js b/services/api/src/resources/task/sql.js index 61b618bd3b..7a82c8a7bc 100644 --- a/services/api/src/resources/task/sql.js +++ b/services/api/src/resources/task/sql.js @@ -69,12 +69,6 @@ const Sql /* : SqlObj */ = { .join('project', 'environment.project', '=', 'project.id') .where('task.id', id) .toString(), - selectPermsForEnvironment: (id /* : number */) => - knex('environment') - .select({ pid: 'project.id', cid: 'project.customer' }) - .join('project', 'environment.project', '=', 'project.id') - .where('environment.id', id) - .toString(), }; module.exports = Sql; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index f6fc7bc779..185b522fc5 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -908,6 +908,15 @@ const typeDefs = gql` addEnvVariable(input: EnvVariableInput!): EnvKeyValue deleteEnvVariable(input: DeleteEnvVariableInput!): String addTask(input: TaskInput!): Task + taskDrushArchiveDump(environment: Int!): Task + taskDrushSqlSync( + sourceEnvironment: Int! + destinationEnvironment: Int! + ): Task + taskDrushRsyncFiles( + sourceEnvironment: Int! + destinationEnvironment: Int! + ): Task deleteTask(input: DeleteTaskInput!): String updateTask(input: UpdateTaskInput): Task setEnvironmentServices(input: SetEnvironmentServicesInput!): [EnvironmentService] From f80feaaa33ed04db4ea95487574a4351e18b66ec Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 2 Dec 2018 18:18:17 -0600 Subject: [PATCH 02/16] Increase storage size of task commands --- .../01-migrations.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql index 53553bec91..c5438ec42e 100644 --- a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql +++ b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql @@ -568,6 +568,26 @@ CREATE OR REPLACE PROCEDURE END; $$ +CREATE OR REPLACE PROCEDURE + convert_task_command_to_text() + + BEGIN + DECLARE column_type varchar(50); + + SELECT DATA_TYPE INTO column_type + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + table_name = 'task' + AND table_schema = 'infrastructure' + AND column_name = 'command'; + + IF (column_type = 'varchar') THEN + ALTER TABLE task + MODIFY command text NOT NULL; + END IF; + END; +$$ + DELIMITER ; CALL add_production_environment_to_project(); @@ -597,6 +617,7 @@ CALL add_active_systems_task_to_project(); CALL add_default_value_to_task_status(); CALL add_scope_to_env_vars(); CALL add_deleted_to_environment_backup(); +CALL convert_task_command_to_text(); -- Drop legacy SSH key procedures DROP PROCEDURE IF EXISTS CreateProjectSshKey; From b305b56c8fd775d0910129b6cafeb1d47bd9d7af Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 2 Dec 2018 18:18:17 -0600 Subject: [PATCH 03/16] Invoke a shell to allow more complicated task commands --- services/openshiftjobs/src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/openshiftjobs/src/index.js b/services/openshiftjobs/src/index.js index abfee38446..f5a56718e6 100644 --- a/services/openshiftjobs/src/index.js +++ b/services/openshiftjobs/src/index.js @@ -158,7 +158,9 @@ const messageConsumer = async msg => { '/sbin/tini', '--', '/lagoon/entrypoints.sh', - ...task.command.split(' '), + '/bin/sh', + '-c', + task.command, ]); taskPodSpec = R.pipe( From 60506746490e1d3b988f651bd6ecca91854caad5 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sun, 2 Dec 2018 18:18:17 -0600 Subject: [PATCH 04/16] taskDrushArchiveDump saves to PV location and gives download instructions --- services/api/src/resources/task/resolvers.js | 22 ++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/services/api/src/resources/task/resolvers.js b/services/api/src/resources/task/resolvers.js index 13ee10f8c5..ec8f450629 100644 --- a/services/api/src/resources/task/resolvers.js +++ b/services/api/src/resources/task/resolvers.js @@ -240,21 +240,31 @@ const updateTask = async ( const taskDrushArchiveDump = async ( root, { - environment, + environment: environmentId, }, { credentials, }, ) => { - await envValidators.environmentExists(environment); - await envValidators.userAccessEnvironment(credentials, environment); - await envValidators.environmentHasService(environment, 'cli'); + await envValidators.environmentExists(environmentId); + await envValidators.userAccessEnvironment(credentials, environmentId); + await envValidators.environmentHasService(environmentId, 'cli'); + + const environment = await environmentHelpers.getEnvironmentById(environmentId); + const filename = `drupal-${Date.now()}-${Math.floor(Math.random() * 998) + 1}.tar.gz`; + const command = String.raw`drush status --format=yaml | \ +grep '%files' | \ +awk '{print $2}' | \ +xargs -I_path drush ard --pipe --destination=_path/private/${filename} | \ +xargs -I_file -- printf 'Your archive has been saved to _file.\nYou can download it by running "drush rsync @${ + environment.name + }:_file ./".'`; const taskData = await Helpers.addTask({ name: 'Drush archive-dump', - environment, + environment: environmentId, service: 'cli', - command: 'drush archive-dump', + command, execute: true, }); From 2d6e93d424bb04daa80f6dd155e391d1361b292c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:36 -0600 Subject: [PATCH 05/16] Switch to apollo-server-express for graphql server --- services/api/package.json | 2 +- services/api/src/apolloServer.js | 25 ++ services/api/src/app.js | 3 + services/api/src/routes/graphql.js | 22 -- services/api/src/routes/index.js | 4 - yarn.lock | 459 +++++++++++++++++++++++++++-- 6 files changed, 464 insertions(+), 51 deletions(-) create mode 100644 services/api/src/apolloServer.js delete mode 100644 services/api/src/routes/graphql.js diff --git a/services/api/package.json b/services/api/package.json index cbb4a4bfde..400157d6d1 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -19,6 +19,7 @@ "license": "MIT", "dependencies": { "@lagoon/commons": "4.0.0", + "apollo-server-express": "^2.2.5", "body-parser": "^1.18.2", "camelcase-keys": "^4.2.0", "compression": "^1.7.1", @@ -28,7 +29,6 @@ "elasticsearch": "^15.0.0", "es7-sleep": "^1.0.0", "express": "^4.16.1", - "express-graphql": "^0.6.11", "flow-remove-types": "^1.2.3", "got": "^9.2.2", "graphql": "^0.13.2", diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js new file mode 100644 index 0000000000..2c14bc46be --- /dev/null +++ b/services/api/src/apolloServer.js @@ -0,0 +1,25 @@ +// @flow + +const { ApolloServer } = require('apollo-server-express'); +const logger = require('./logger'); +const typeDefs = require('./typeDefs'); +const resolvers = require('./resolvers'); + +const apolloServer = new ApolloServer({ + typeDefs, + resolvers, + debug: process.env.NODE_ENV === 'development', + context: ({ req }) => ({ + credentials: req.credentials, + }), + formatError: (error) => { + logger.warn(error.message); + return { + message: error.message, + locations: error.locations, + path: error.path, + }; + }, +}); + +module.exports = apolloServer; diff --git a/services/api/src/app.js b/services/api/src/app.js index 11a6a22837..d930f54324 100644 --- a/services/api/src/app.js +++ b/services/api/src/app.js @@ -9,6 +9,7 @@ const logger = require('./logger'); const createRouter = require('./routes'); const { authKeycloakMiddleware } = require('./authKeycloakMiddleware'); const { createAuthMiddleware } = require('./authMiddleware'); +const apolloServer = require('./apolloServer'); /* :: type CreateAppArgs = { @@ -58,6 +59,8 @@ const createApp = (args /* : CreateAppArgs */) => { // Add routes. app.use('/', createRouter()); + apolloServer.applyMiddleware({ app }); + return app; }; diff --git a/services/api/src/routes/graphql.js b/services/api/src/routes/graphql.js deleted file mode 100644 index c197de6d98..0000000000 --- a/services/api/src/routes/graphql.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -const graphql = require('express-graphql'); -const { schema } = require('../schema'); -const logger = require('../logger'); - -const graphqlRoute = graphql({ - graphiql: process.env.NODE_ENV === 'development', - pretty: true, - schema, - formatError: (error) => { - logger.warn(error.message); - return { - message: error.message, - locations: error.locations, - path: error.path, - }; - } - , -}); - -module.exports = [graphqlRoute]; diff --git a/services/api/src/routes/index.js b/services/api/src/routes/index.js index 490bb865ce..75bdd0bf45 100644 --- a/services/api/src/routes/index.js +++ b/services/api/src/routes/index.js @@ -3,7 +3,6 @@ const express = require('express'); const statusRoute = require('./status'); const keysRoute = require('./keys'); -const graphqlRoute = require('./graphql'); /* :: import type { $Request, $Response } from 'express'; @@ -23,9 +22,6 @@ function createRouter() { // Return keys of all customers router.post('/keys', ...keysRoute); - // Enable graphql requests. - router.all('/graphql', ...graphqlRoute); - return router; } diff --git a/yarn.lock b/yarn.lock index 16fca586df..437ae9fd62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,27 @@ resolved "https://registry.yarnpkg.com/8fold-marked/-/8fold-marked-0.3.9.tgz#bb89c645612f8ccfaffac1ca6e3c11f168c9cf59" integrity sha512-OmVTXzmvQk/WuVZnOa+sT6wpkLrkPrjYanXS3X0ib1yk6315iwuPmvwKkOOdpqznYmavyT2ZCDOo6MeFe4xSog== +"@apollographql/apollo-tools@^0.2.6": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.2.8.tgz#f755baa3576eabdd93afa2782be61f5ae8a856dc" + integrity sha512-A7FTUigtpGCFBaLT1ILicdjM6pZ7LQNw7Vgos0t4aLYtvlKO/L1nMi/NO7bPypzZaJSToTgcxHJPRydP1Md+Kw== + dependencies: + apollo-env "0.2.5" + +"@apollographql/apollo-upload-server@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@apollographql/apollo-upload-server/-/apollo-upload-server-5.0.3.tgz#8558c378ff6457de82147e5072c96a6b242773b7" + integrity sha512-tGAp3ULNyoA8b5o9LsU2Lq6SwgVPUOKAqKywu2liEtTvrFSGPrObwanhYwArq3GPeOqp2bi+JknSJCIU3oQN1Q== + dependencies: + "@babel/runtime-corejs2" "^7.0.0-rc.1" + busboy "^0.2.14" + object-path "^0.11.4" + +"@apollographql/graphql-playground-html@^1.6.6": + version "1.6.6" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3" + integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ== + "@babel/cli@^7.0.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.1.0.tgz#a9429fd63911711b0fa93ae50d73beee6c42aef8" @@ -1265,6 +1286,14 @@ pirates "^4.0.0" source-map-support "^0.5.9" +"@babel/runtime-corejs2@^7.0.0-rc.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.2.0.tgz#5ccd722b72d2c18c6a7224b5751f4b9816b60ada" + integrity sha512-kPfmKoRI8Hpo5ZJGACWyrc9Eq1j3ZIUpUAQT2yH045OuYpccFJ9kYA/eErwzOM2jeBG1sC8XX1nl1EArtuM8tg== + dependencies: + core-js "^2.5.7" + regenerator-runtime "^0.12.0" + "@babel/runtime@7.0.0-beta.42": version "7.0.0-beta.42" resolved "http://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.42.tgz#352e40c92e0460d3e82f49bd7e79f6cda76f919f" @@ -1436,6 +1465,59 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" integrity sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + "@sindresorhus/is@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.11.0.tgz#a65970040a5b55c4713452666703b92a6c331fdb" @@ -1468,6 +1550,13 @@ dependencies: defer-to-connect "^1.0.1" +"@types/accepts@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + "@types/async@2.0.50": version "2.0.50" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb" @@ -1481,6 +1570,28 @@ "@types/express" "*" "@types/node" "*" +"@types/body-parser@1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.4.tgz#50991a759a29c0b89492751008c6af7a7c8267b0" + integrity sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw== + dependencies: + "@types/express" "*" + "@types/debug@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.30.tgz#dc1e40f7af3b9c815013a7860e6252f6352a84df" @@ -1515,6 +1626,15 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/express@4.16.0": + version "4.16.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" + integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + "@types/express@~4.0.34": version "4.0.39" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.39.tgz#1441f21d52b33be8d4fa8a865c15a6a91cd0fa09" @@ -1529,6 +1649,11 @@ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.12.6.tgz#3d619198585fcabe5f4e1adfb5cf5f3388c66c13" integrity sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ== +"@types/long@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" + integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== + "@types/mime@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" @@ -1539,6 +1664,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" integrity sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw== +"@types/node@^10.1.0": + version "10.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47" + integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A== + "@types/serve-static@*": version "1.13.1" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.1.tgz#1d2801fa635d274cd97d4ec07e26b21b44127492" @@ -1547,6 +1677,14 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/ws@^6.0.0": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28" + integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q== + dependencies: + "@types/events" "*" + "@types/node" "*" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -1562,7 +1700,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@^1.3.0, accepts@~1.3.4: +accepts@^1.3.5, accepts@~1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= @@ -1757,6 +1895,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apollo-cache-control@0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.3.3.tgz#ad71d8f786e06f0275b2432004c15c2d37c48484" + integrity sha512-X6JhKfIaMLfl2jpsK/880BflXA+2lmm2sAsOZL4Bn2VrMsDtOssI1Ij9vNRbch9k9cA4WJvKed7Sql/wUIa1Eg== + dependencies: + apollo-server-env "2.2.0" + graphql-extensions "0.3.3" + apollo-cache-inmemory@^1.3.9: version "1.3.9" resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.3.9.tgz#10738ba6a04faaeeb0da21bbcc1f7c0b5902910c" @@ -1788,6 +1934,40 @@ apollo-client@^2.4.5: optionalDependencies: "@types/async" "2.0.50" +apollo-datasource@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.2.1.tgz#3ecef4efe64f7a04a43862f32027d38ac09e142c" + integrity sha512-r185+JTa5KuF1INeTAk7AEP76zwMN6c8Ph1lmpzJMNwBUEzTGnLClrccCskCBx4SxfnkdKbuQdwn9JwCJUWrdg== + dependencies: + apollo-server-caching "0.2.1" + apollo-server-env "2.2.0" + +apollo-engine-reporting-protobuf@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.1.0.tgz#fbc220cac2a3b7800ffc155d7e54c21c56b7848e" + integrity sha512-GReJtAYTmpwg0drb9VgFtqObYYTCHkJhlHEYCeXY8bJV4fOgXsAZ7CIXR9nPKO0mBaoHIHaGYvXGcyCLrZ36VA== + dependencies: + protobufjs "^6.8.6" + +apollo-engine-reporting@0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-0.1.3.tgz#85ad6ffd71db8f877202ce8b3d7dbfa7cabfbcf9" + integrity sha512-VkjiifHMHIAxydXecT+ck0WtqpFIsMlylKnKeuNAXfIfAXHX/JYtLhbArTTyhDunLrphMiUewfFv9P0K+aX2jw== + dependencies: + apollo-engine-reporting-protobuf "0.1.0" + apollo-server-env "2.2.0" + async-retry "^1.2.1" + graphql-extensions "0.3.3" + lodash "^4.17.10" + +apollo-env@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.2.5.tgz#162c785bccd2aea69350a7600fab4b7147fc9da5" + integrity sha512-Gc7TEbwCl7jJVutnn8TWfzNSkrrqyoo0DP92BQJFU9pZbJhpidoXf2Sw1YwOJl82rRKH3ujM3C8vdZLOgpFcFA== + dependencies: + core-js "^3.0.0-beta.3" + node-fetch "^2.2.0" + apollo-link-dedup@^1.0.0: version "1.0.9" resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.9.tgz#3c4e4af88ef027cbddfdb857c043fd0574051dad" @@ -1843,6 +2023,84 @@ apollo-link@^1.2.3: apollo-utilities "^1.0.0" zen-observable-ts "^0.8.10" +apollo-server-caching@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.2.1.tgz#7e67f8c8cac829e622b394f0fb82579cabbeadfd" + integrity sha512-+U9F3X297LL8Gqy6ypfDNEv/DfV/tDht9Dr2z3AMaEkNW1bwO6rmdDL01zYxDuVDVq6Z3qSiNCSO2pXE2F0zmA== + dependencies: + lru-cache "^5.0.0" + +apollo-server-core@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.2.5.tgz#bf1538c10213be38a37dd8e6461461f7b808c57e" + integrity sha512-obz6VSJI7vSR+pEAZFwqOe/HAOuF4l1fYU9WNtVcQvxaKhykDgcu+byO0sXrOf/iB7uUIyaFdhinwzuwkqB8XQ== + dependencies: + "@apollographql/apollo-tools" "^0.2.6" + "@apollographql/apollo-upload-server" "^5.0.3" + "@apollographql/graphql-playground-html" "^1.6.6" + "@types/ws" "^6.0.0" + apollo-cache-control "0.3.3" + apollo-datasource "0.2.1" + apollo-engine-reporting "0.1.3" + apollo-server-caching "0.2.1" + apollo-server-env "2.2.0" + apollo-server-errors "2.2.0" + apollo-server-plugin-base "0.1.5" + apollo-tracing "0.3.3" + graphql-extensions "0.3.5" + graphql-subscriptions "^1.0.0" + graphql-tag "^2.9.2" + graphql-tools "^4.0.0" + json-stable-stringify "^1.0.1" + lodash "^4.17.10" + subscriptions-transport-ws "^0.9.11" + ws "^6.0.0" + +apollo-server-env@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.2.0.tgz#5eec5dbf46581f663fd6692b2e05c7e8ae6d6034" + integrity sha512-wjJiI5nQWPBpNmpiLP389Ezpstp71szS6DHAeTgYLb/ulCw3CTuuA+0/E1bsThVWiQaDeHZE0sE3yI8q2zrYiA== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" + +apollo-server-errors@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5" + integrity sha512-gV9EZG2tovFtT1cLuCTavnJu2DaKxnXPRNGSTo+SDI6IAk6cdzyW0Gje5N2+3LybI0Wq5KAbW6VLei31S4MWmg== + +apollo-server-express@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.2.5.tgz#9d27d68b3b1cf2f96a107a3091ecdea4012745a2" + integrity sha512-2SNlY8CNmYlbRJfn0iK4wesjqX3X9YIFhyok4sQ80n/gm24QMwZkFcPP+NLv+1lxvwyJYMwEFQPIBvkLRoUFXQ== + dependencies: + "@apollographql/apollo-upload-server" "^5.0.3" + "@apollographql/graphql-playground-html" "^1.6.6" + "@types/accepts" "^1.3.5" + "@types/body-parser" "1.17.0" + "@types/cors" "^2.8.4" + "@types/express" "4.16.0" + accepts "^1.3.5" + apollo-server-core "2.2.5" + body-parser "^1.18.3" + cors "^2.8.4" + graphql-subscriptions "^1.0.0" + graphql-tools "^4.0.0" + type-is "^1.6.16" + +apollo-server-plugin-base@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.1.5.tgz#899c4d7bc0d9a6d9f1181cc83a791479409086f8" + integrity sha512-be77TaN9l16ZVG1tBl8Re3lJfUZ6B2T3DdEXnu6fjQwUuBdu3Y4MQR6B1TLhbuTb9DUkcSKZ3h5C55dIjvb2Vg== + +apollo-tracing@0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.3.3.tgz#b819942180480c1c4d89e613cf2eff8f6d8b595a" + integrity sha512-gsTYgDVjtMlnomPq46aky7yk8XshCQfj9rxalCCismLlMomVW44fq+8GKQnZIkFOwiAsazRy4dzZ0cBbygA9sA== + dependencies: + apollo-server-env "2.2.0" + graphql-extensions "0.3.3" + apollo-utilities@1.0.25, apollo-utilities@^1.0.25: version "1.0.25" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.25.tgz#899b00f5f990fb451675adf84cb3de82eb6372ea" @@ -2019,7 +2277,7 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async-retry@^1.2.3: +async-retry@^1.2.1, async-retry@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0" integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q== @@ -2350,6 +2608,11 @@ babylon@^6.15.0, babylon@^6.18.0: resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +backo2@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -2467,6 +2730,22 @@ body-parser@1.18.2, body-parser@^1.18.2: raw-body "2.3.2" type-is "~1.6.15" +body-parser@^1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + boolify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/boolify/-/boolify-1.0.1.tgz#b5c09e17cacd113d11b7bb3ed384cc012994d86b" @@ -2706,6 +2985,14 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +busboy@^0.2.14: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3234,7 +3521,7 @@ content-disposition@0.5.2: resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= -content-type@^1.0.4, content-type@~1.0.4: +content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== @@ -3295,6 +3582,11 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3, core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== +core-js@^3.0.0-beta.3: + version "3.0.0-beta.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.3.tgz#b0f22009972b8c6c04550ebf38513ca4b3cc9559" + integrity sha512-kM/OfrnMThP5PwGAj5HhQLdjUqzjrllqN2EVnk/X9qrLsfYjR2hzZ+E/8CzH0xuosexZtqMTLQrk//BULrBj9w== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3678,7 +3970,7 @@ depd@1.1.1: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= -depd@~1.1.1: +depd@~1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -3723,6 +4015,14 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -4408,6 +4708,11 @@ eventemitter3@^1.1.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= +eventemitter3@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== + events@^1.0.0, events@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -4505,16 +4810,6 @@ expect@^23.6.0: jest-message-util "^23.4.0" jest-regex-util "^23.3.0" -express-graphql@^0.6.11: - version "0.6.12" - resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.6.12.tgz#dfcb2058ca72ed5190b140830ad8cdbf76a9128a" - integrity sha512-ouLWV0hRw4hnaLtXzzwhdC79ewxKbY2PRvm05mPc/zOH5W5WVCHDQ1SmNxEPBQdUeeSNh29aIqW9zEQkA3kMuA== - dependencies: - accepts "^1.3.0" - content-type "^1.0.4" - http-errors "^1.3.0" - raw-body "^2.3.2" - express-validator@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-4.3.0.tgz#60218a5778c59d5e778b89ae4e00b76f8510ef78" @@ -5253,6 +5548,20 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= +graphql-extensions@0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.3.3.tgz#277efe11976bbdfd59915551606a2d550247bb45" + integrity sha512-pudOaHq7Ok+rh1ElzlqFaoYZWGefUNsqn/jX6eKns7rl0VHuB4qZBfhpVLTpquJpM6Y19/hsCYZNPfnUVMFIiA== + dependencies: + "@apollographql/apollo-tools" "^0.2.6" + +graphql-extensions@0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.3.5.tgz#95b742185d0016a9d65385a7a9e10753eadf0537" + integrity sha512-jpWSUIr27iOTR5JYu+dEMz74oZhOj8Xy+6lNopluiIu+ObEVSHW0czb2Jlcy3rOSTEPcibnpStO4F4/64IBqeQ== + dependencies: + "@apollographql/apollo-tools" "^0.2.6" + graphql-iso-date@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.5.0.tgz#55a1be0efa8d28c1453afd2eb5ce1d052189a513" @@ -5263,7 +5572,14 @@ graphql-list-fields@^2.0.2: resolved "https://registry.yarnpkg.com/graphql-list-fields/-/graphql-list-fields-2.0.2.tgz#a4ade3cfa2028a2ac32d3f2870f5f8c5b5d5b466" integrity sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q== -graphql-tag@^2.10.0: +graphql-subscriptions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8" + integrity sha512-+ytmryoHF1LVf58NKEaNPRUzYyXplm120ntxfPcgOBC7TnK7Tv/4VRHeh4FAR9iL+O1bqhZs4nkibxQ+OA5cDQ== + dependencies: + iterall "^1.2.1" + +graphql-tag@^2.10.0, graphql-tag@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae" integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w== @@ -5279,6 +5595,17 @@ graphql-tools@^2.5.0: iterall "^1.1.3" uuid "^3.1.0" +graphql-tools@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.3.tgz#23b5cb52c519212b1b2e4630a361464396ad264b" + integrity sha512-NNZM0WSnVLX1zIMUxu7SjzLZ4prCp15N5L2T2ro02OVyydZ0fuCnZYRnx/yK9xjGWbZA0Q58yEO//Bv/psJWrg== + dependencies: + apollo-link "^1.2.3" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + graphql@^0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" @@ -5543,7 +5870,7 @@ http-cache-semantics@^4.0.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#2d0069a73c36c80e3297bc3a0cadd669b78a69ce" integrity sha512-NtexGRtaV5z3ZUX78W9UDTOJPBdpqms6RmwQXmOhHws7CuQK3cqIoQtnmeqi1VvVD6u6eMMRL0sKE9BCZXTDWQ== -http-errors@1.6.2, http-errors@^1.3.0, http-errors@~1.6.2: +http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= @@ -5553,6 +5880,16 @@ http-errors@1.6.2, http-errors@^1.3.0, http-errors@~1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-errors@1.6.3, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-reasons@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/http-reasons/-/http-reasons-0.1.0.tgz#a953ca670078669dde142ce899401b9d6e85d3b4" @@ -7159,6 +7496,11 @@ lokka@^1.7.0: babel-runtime "6.x.x" uuid "2.x.x" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -7202,6 +7544,13 @@ lru-cache@^4.0.1, lru-cache@^4.1.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + make-dir@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" @@ -8027,6 +8376,11 @@ object-keys@^1.0.11, object-keys@^1.0.12: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== +object-path@^0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" + integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= + object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -8866,6 +9220,25 @@ prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" +protobufjs@^6.8.6: + version "6.8.8" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" + integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.0" + "@types/node" "^10.1.0" + long "^4.0.0" + protocols@^1.1.0, protocols@^1.4.0: version "1.4.6" resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.6.tgz#f8bb263ea1b5fd7a7604d26b8be39bd77678bf8a" @@ -8964,6 +9337,11 @@ qs@6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== +qs@6.5.2, qs@~6.5.1, qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" @@ -8974,11 +9352,6 @@ qs@~6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= -qs@~6.5.1, qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -9035,7 +9408,7 @@ range-parser@^1.0.3, range-parser@~1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= -raw-body@2.3.2, raw-body@^2.3.2: +raw-body@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= @@ -9045,6 +9418,16 @@ raw-body@2.3.2, raw-body@^2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + rc@^1.0.1, rc@^1.1.6: version "1.2.5" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" @@ -9220,7 +9603,7 @@ read-pkg@^3.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@1.x >=1.1.9": +readable-stream@1.1.x, "readable-stream@1.x >=1.1.9": version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= @@ -10247,6 +10630,11 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -10303,6 +10691,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + strftime@~0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/strftime/-/strftime-0.10.0.tgz#b3f0fa419295202a5a289f6d6be9f4909a617193" @@ -10452,6 +10845,17 @@ stylis@^3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.3.tgz#99fdc46afba6af4deff570825994181a5e6ce546" integrity sha512-TxU0aAscJghF9I3V9q601xcK3Uw1JbXvpsBGj/HULqexKOKlOEzzlIpLFRbKkCK990ccuxfXUqmPbIIo7Fq/cQ== +subscriptions-transport-ws@^0.9.11: + version "0.9.15" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d" + integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -10802,7 +11206,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.15: +type-is@^1.6.16, type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== @@ -11553,6 +11957,13 @@ ws@^5.2.0: dependencies: async-limiter "~1.0.0" +ws@^6.0.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" + integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== + dependencies: + async-limiter "~1.0.0" + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" From 3bfe7fa7883490e6dc024de37bc56a0b919ce8b7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:36 -0600 Subject: [PATCH 06/16] Add graphql subscription support to api --- services/api/package.json | 3 +- services/api/src/apolloServer.js | 50 ++++++- services/api/src/app.js | 70 +++------ services/api/src/authKeycloakMiddleware.js | 125 ---------------- services/api/src/authMiddleware.js | 166 +++++++-------------- services/api/src/index.js | 5 +- services/api/src/server.js | 17 +-- services/api/src/util/auth.js | 136 ++++++++++++++++- yarn.lock | 56 +++---- 9 files changed, 286 insertions(+), 342 deletions(-) delete mode 100644 services/api/src/authKeycloakMiddleware.js diff --git a/services/api/package.json b/services/api/package.json index 400157d6d1..ab81bcc4e8 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -20,6 +20,7 @@ "dependencies": { "@lagoon/commons": "4.0.0", "apollo-server-express": "^2.2.5", + "axios": "^0.18.0", "body-parser": "^1.18.2", "camelcase-keys": "^4.2.0", "compression": "^1.7.1", @@ -36,8 +37,8 @@ "graphql-list-fields": "^2.0.2", "graphql-tools": "^2.5.0", "jsonwebtoken": "^8.0.1", + "jwk-to-pem": "^2.0.0", "keycloak-admin": "^1.8.0", - "keycloak-connect": "^4.5.0", "knex": "^0.14.3", "mariasql": "^0.2.6", "moment": "^2.22.2", diff --git a/services/api/src/apolloServer.js b/services/api/src/apolloServer.js index 2c14bc46be..11728b0a15 100644 --- a/services/api/src/apolloServer.js +++ b/services/api/src/apolloServer.js @@ -1,6 +1,8 @@ // @flow -const { ApolloServer } = require('apollo-server-express'); +const R = require('ramda'); +const { ApolloServer, AuthenticationError } = require('apollo-server-express'); +const { getCredentialsForLegacyToken, getCredentialsForKeycloakToken } = require('./util/auth'); const logger = require('./logger'); const typeDefs = require('./typeDefs'); const resolvers = require('./resolvers'); @@ -9,9 +11,49 @@ const apolloServer = new ApolloServer({ typeDefs, resolvers, debug: process.env.NODE_ENV === 'development', - context: ({ req }) => ({ - credentials: req.credentials, - }), + subscriptions: { + onConnect: async (connectionParams, webSocket) => { + const token = R.prop('authToken', connectionParams); + let credentials; + + if (!token) { + throw new AuthenticationError('Auth token missing.'); + } + + try { + credentials = await getCredentialsForKeycloakToken(token); + } catch (e) { + // It might be a legacy token, so continue on. + logger.debug(`Keycloak token auth failed: ${e.message}`); + } + + try { + if (!credentials) { + credentials = await getCredentialsForLegacyToken(token); + } + } catch (e) { + throw new AuthenticationError(e.message); + } + + // Add credentials to context. + return { credentials }; + }, + }, + context: ({ req, connection }) => { + // Websocket requests + if (connection) { + // onConnect must always provide connection.context. + return connection.context; + } + + // HTTP requests + if (!connection) { + return { + // Express middleware must always provide req.credentials. + credentials: req.credentials, + }; + } + }, formatError: (error) => { logger.warn(error.message); return { diff --git a/services/api/src/app.js b/services/api/src/app.js index d930f54324..dc1bba4157 100644 --- a/services/api/src/app.js +++ b/services/api/src/app.js @@ -7,61 +7,35 @@ const cors = require('cors'); const { json } = require('body-parser'); const logger = require('./logger'); const createRouter = require('./routes'); -const { authKeycloakMiddleware } = require('./authKeycloakMiddleware'); -const { createAuthMiddleware } = require('./authMiddleware'); +const authMiddleware = require('./authMiddleware'); const apolloServer = require('./apolloServer'); -/* :: -type CreateAppArgs = { - store?: Object, - jwtSecret: string, - jwtAudience: string, -}; -*/ +const app = express(); -const createApp = (args /* : CreateAppArgs */) => { - const { - // store, - jwtSecret, - jwtAudience, - } = args; - const app = express(); +// Use compression (gzip) for responses. +app.use(compression()); - // Use compression (gzip) for responses. - app.use(compression()); +// Automatically decode json. +app.use(json()); - // Automatically decode json. - app.use(json()); +// Add custom configured logger (morgan through winston). +app.use( + morgan('combined', { + stream: { + write: message => logger.info(message), + }, + }), +); - // Add custom configured logger (morgan through winston). - app.use( - morgan('combined', { - stream: { - write: message => logger.info(message), - }, - }), - ); +// TODO: Restrict requests to lagoon domains? +app.use(cors()); - // TODO: Restrict requests to lagoon domains? - app.use(cors()); +// $FlowFixMe +app.use(authMiddleware); - // $FlowFixMe - app.use(authKeycloakMiddleware()); +// Add routes. +app.use('/', createRouter()); - app.use( - createAuthMiddleware({ - baseUri: 'http://auth-server:3000', - jwtSecret, - jwtAudience, - }), - ); +apolloServer.applyMiddleware({ app }); - // Add routes. - app.use('/', createRouter()); - - apolloServer.applyMiddleware({ app }); - - return app; -}; - -module.exports = createApp; +module.exports = app; diff --git a/services/api/src/authKeycloakMiddleware.js b/services/api/src/authKeycloakMiddleware.js deleted file mode 100644 index 2e43fc66e8..0000000000 --- a/services/api/src/authKeycloakMiddleware.js +++ /dev/null @@ -1,125 +0,0 @@ -// @flow - -const Keycloak = require('keycloak-connect'); -const Setup = require('keycloak-connect/middleware/setup'); -const GrantAttacher = require('keycloak-connect/middleware/grant-attacher'); -const R = require('ramda'); -const { getPermissionsForUser } = require('./util/auth'); - -const lagoonRoutes = - (process.env.LAGOON_ROUTES && process.env.LAGOON_ROUTES.split(',')) || []; - -const lagoonKeycloakRoute = lagoonRoutes.find(routes => - routes.includes('keycloak-'), -); - -const keycloak = new Keycloak( - {}, - { - realm: 'lagoon', - serverUrl: lagoonKeycloakRoute - ? `${lagoonKeycloakRoute}/auth` - : 'http://docker.for.mac.localhost:8088/auth', - clientId: 'lagoon-ui', - publicClient: true, - bearerOnly: true, - }, -); - -// Override default of returning a 403 -keycloak.accessDenied = (req, res, next) => { - console.log('keycloak.accessDenied'); - next(); -}; - -/* :: -import type { $Request, $Response, Middleware, NextFunction } from 'express'; - -type CreateAuthMiddlewareArgs = { - baseUri: string, - jwtSecret: string, - jwtAudience: string, -}; - -class Request extends express$Request { - credentials: any - kauth: any -}; - -type AuthWithKeycloakFn = - ( - // To allow extending the request object with Flow - Request, - $Response, - NextFunction - ) => - Promise - -*/ - -const authWithKeycloak /* : AuthWithKeycloakFn */ = async (req, res, next) => { - if (!req.kauth.grant) { - next(); - return; - } - - try { - // Admins have full access and don't need a list of permissions - if ( - R.contains( - 'admin', - req.kauth.grant.access_token.content.realm_access.roles, - ) - ) { - req.credentials = { - role: 'admin', - permissions: {}, - }; - } else { - const { - content: { - lagoon: { user_id: userId }, - }, - } = req.kauth.grant.access_token; - - const permissions = await getPermissionsForUser(userId); - - if (R.isEmpty(permissions)) { - res.status(401).send({ - errors: [ - { - message: `Unauthorized - No permissions for user id ${userId}`, - }, - ], - }); - return; - } - - req.credentials = { - role: 'none', - userId, - // Read and write permissions - permissions, - }; - } - next(); - } catch (e) { - res.status(403).send({ - errors: [ - { - message: `Forbidden - Invalid Keycloak Token: ${e.message}`, - }, - ], - }); - } -}; - -const authKeycloakMiddleware = () /* : Array */ => [ - Setup, - GrantAttacher(keycloak), - authWithKeycloak, -]; - -module.exports = { - authKeycloakMiddleware, -}; diff --git a/services/api/src/authMiddleware.js b/services/api/src/authMiddleware.js index 8bed4b0f20..2303d28ec7 100644 --- a/services/api/src/authMiddleware.js +++ b/services/api/src/authMiddleware.js @@ -1,15 +1,18 @@ // @flow -const jwt = require('jsonwebtoken'); -const R = require('ramda'); -const logger = require('./logger'); -const { getPermissionsForUser } = require('./util/auth'); - /* :: -import type { $Application } from 'express'; -import type { CredMaybe } from './resources'; +import type { $Request, $Response, NextFunction } from 'express'; + +class Request extends express$Request { + credentials: any + authToken: string +}; */ +const R = require('ramda'); +const logger = require('./logger'); +const { getCredentialsForKeycloakToken, getCredentialsForLegacyToken } = require('./util/auth'); + const parseBearerToken = R.compose( R.ifElse( splits => @@ -26,59 +29,13 @@ const parseBearerToken = R.compose( R.defaultTo(''), ); -const decodeToken = ( - token, - secret, -) /* : ?{aud: string, role: string, userId: number} */ => { - try { - const decoded = jwt.verify(token, secret); - return decoded; - } catch (e) { - return null; - } -}; - -/* :: -import type { $Request, $Response, NextFunction } from 'express'; - -type CreateAuthMiddlewareArgs = { - baseUri: string, - jwtSecret: string, - jwtAudience: string, -}; - -class Request extends express$Request { - credentials: any -}; - -type CreateAuthMiddlewareFn = - CreateAuthMiddlewareArgs => - ( - // To allow extending the request object with Flow - Request, - $Response, - NextFunction - ) => - Promise - -*/ - -const createAuthMiddleware /* : CreateAuthMiddlewareFn */ = ({ - jwtSecret, - jwtAudience, -}) => async (req, res, next) => { - // Allow access to status without auth +const prepareToken = async (req /* : Request */, res /* : $Response */, next /* : NextFunction */) => { + // Allow access to status without auth. if (req.url === '/status') { next(); return; } - // Allow keycloak authenticated sessions - if (req.credentials) { - next(); - return; - } - const token = parseBearerToken(req.get('Authorization')); if (token == null) { @@ -89,66 +46,45 @@ const createAuthMiddleware /* : CreateAuthMiddlewareFn */ = ({ return; } - let decoded = ''; + req.authToken = token; + + next(); +}; + +const keycloak = async (req /* : Request */, res /* : $Response */, next /* : NextFunction */) => { + // Allow access to status without auth. + if (req.url === '/status') { + next(); + return; + } + try { - decoded = decodeToken(token, jwtSecret); + const credentials = await getCredentialsForKeycloakToken(req.authToken); + + req.credentials = credentials; } catch (e) { - const errorMessage = `Error while decoding auth token: ${e.message}`; - logger.debug(errorMessage); - res.status(500).send({ - errors: [ - { - message: errorMessage, - }, - ], - }); + // It might be a legacy token, so continue on. + logger.debug(`Keycloak token auth failed: ${e.message}`); + } + + next(); +}; + +const legacy = async (req /* : Request */, res /* : $Response */, next /* : NextFunction */) => { + // Allow access to status without auth. + if (req.url === '/status') { + next(); + return; + } + + // Allow keycloak authenticated sessions + if (req.credentials) { + next(); return; } try { - if (decoded == null) { - throw new Error('Decoding token resulted in "null" or "undefined"'); - } - - const { userId, role = 'none', aud } = decoded; - - if (jwtAudience && aud !== jwtAudience) { - logger.info(`Invalid token with aud attribute: "${aud || ''}"`); - res.status(500).send({ - errors: [{ message: 'Auth token audience mismatch' }], - }); - return; - } - - // We need this, since non-admin credentials are required to have an user id - let nonAdminCreds = {}; - - if (role !== 'admin') { - const permissions = await getPermissionsForUser(userId); - - if (R.isEmpty(permissions)) { - res.status(401).send({ - errors: [ - { - message: `Unauthorized - No permissions for user id ${userId}`, - }, - ], - }); - return; - } - - nonAdminCreds = { - userId, - // Read and write permissions - permissions, - }; - } - - const credentials /* : CredMaybe */ = { - role, - permissions: {}, - ...nonAdminCreds, - }; + const credentials = await getCredentialsForLegacyToken(req.authToken); req.credentials = credentials; @@ -160,6 +96,12 @@ const createAuthMiddleware /* : CreateAuthMiddlewareFn */ = ({ } }; -module.exports = { - createAuthMiddleware, -}; +const authMiddleware = [ + prepareToken, + // First attempt to validate token with keycloak. + keycloak, + // Then validate legacy token. + legacy, +]; + +module.exports = authMiddleware; diff --git a/services/api/src/index.js b/services/api/src/index.js index 8bf54ad57d..703e0c5196 100644 --- a/services/api/src/index.js +++ b/services/api/src/index.js @@ -35,10 +35,7 @@ initSendToLagoonTasks(); ); } - await createServer({ - jwtSecret: JWTSECRET, - jwtAudience: JWTAUDIENCE, - }); + await createServer(); logger.debug('Finished booting the application.'); } catch (e) { diff --git a/services/api/src/server.js b/services/api/src/server.js index cda3edb407..33221eb26e 100644 --- a/services/api/src/server.js +++ b/services/api/src/server.js @@ -3,7 +3,8 @@ const http = require('http'); const util = require('util'); const logger = require('./logger'); -const createApp = require('./app'); +const app = require('./app'); +const apolloServer = require('./apolloServer'); const normalizePort = value => { const port = parseInt(value, 10); @@ -15,19 +16,13 @@ const normalizePort = value => { return false; }; -/* :: -type CreateServerArgs = { - store?: Object, - jwtSecret: string, - jwtAudience: string, -}; -*/ - -const createServer = async (args /* : CreateServerArgs */) => { +const createServer = async () => { logger.debug('Starting to boot the server.'); const port = normalizePort(process.env.PORT || '3000'); - const server = http.createServer(createApp(args)); + const server = http.createServer(app); + + apolloServer.installSubscriptionHandlers(server); const listen = util.promisify(server.listen).bind(server); await listen(port); diff --git a/services/api/src/util/auth.js b/services/api/src/util/auth.js index 96dc444394..0649e221b5 100644 --- a/services/api/src/util/auth.js +++ b/services/api/src/util/auth.js @@ -1,7 +1,14 @@ +const util = require('util'); const R = require('ramda'); +const jwt = require('jsonwebtoken'); +const jwkToPem = require('jwk-to-pem'); +const axios = require('axios'); +const logger = require('../logger'); const sqlClient = require('../clients/sqlClient'); const { query, prepare } = require('../util/db'); +const { JWTSECRET, JWTAUDIENCE } = process.env; + const notEmptyOrNaN /* : Function */ = R.allPass([ R.compose( R.not, @@ -29,7 +36,7 @@ const splitCommaSeparatedPermissions /* : (?string) => Array */ = R.com const getPermissions = async args => { const prep = prepare( sqlClient, - 'SELECT user_id, projects, customers FROM permission WHERE user_id = :user_id', + 'SELECT projects, customers FROM permission WHERE user_id = :user_id', ); const rows = await query(sqlClient, prep(args)); @@ -53,7 +60,134 @@ const getPermissionsForUser = async userId => { return permissions; }; +// Attempt to load signing key from Keycloak API. +const fetchKeycloakKey = async (header, cb) => { + const lagoonRoutes = + (process.env.LAGOON_ROUTES && process.env.LAGOON_ROUTES.split(',')) || []; + + const lagoonKeycloakRoute = lagoonRoutes.find(routes => + routes.includes('keycloak-'), + ); + + const authServerUrl = lagoonKeycloakRoute + ? `${lagoonKeycloakRoute}/auth` + : 'http://docker.for.mac.localhost:8088/auth'; + + try { + const response = await axios.get(`${authServerUrl}/realms/lagoon/protocol/openid-connect/certs`); + const jwks = response.data.keys; + + const jwk = jwks.find(key => key.kid === header.kid); + + if (!jwk) { + throw new Error('No keycloak key found for realm lagoon.'); + } + + cb(null, jwkToPem(jwk)); + } catch (e) { + cb(e); + } +}; + +const getCredentialsForKeycloakToken = async token => { + const decodeToken = util.promisify(jwt.verify); + + // Check for a valid keycloak token before cryptographically verifying it to + // save a network request. + const { azp } = jwt.decode(token); + if (!azp || azp !== 'lagoon-ui') { + throw new Error('Not a recognized Keycloak token.'); + } + + let decoded = ''; + try { + decoded = await decodeToken(token, fetchKeycloakKey); + + if (decoded == null) { + throw new Error('Decoding token resulted in "null" or "undefined".'); + } + } catch (e) { + throw new Error(`Error decoding token: ${e.message}`); + } + + let nonAdminCreds = {}; + + if (!R.contains( + 'admin', + decoded.realm_access.roles, + )) { + const { + lagoon: { user_id: userId }, + } = decoded; + const permissions = await getPermissionsForUser(userId); + + if (R.isEmpty(permissions)) { + throw new Error(`No permissions for user id ${userId}.`); + } + + nonAdminCreds = { + userId, + role: 'none', + // Read and write permissions + permissions, + }; + } + + return { + role: 'admin', + permissions: {}, + ...nonAdminCreds, + }; +}; + +const getCredentialsForLegacyToken = async token => { + let decoded = ''; + try { + decoded = jwt.verify(token, JWTSECRET); + + if (decoded == null) { + throw new Error('Decoding token resulted in "null" or "undefined".'); + } + + const { aud } = decoded; + + if (JWTAUDIENCE && aud !== JWTAUDIENCE) { + logger.info(`Invalid token with aud attribute: "${aud || ''}"`); + throw new Error('Token audience mismatch.'); + } + } catch (e) { + throw new Error(`Error decoding token: ${e.message}`); + } + + const { userId, role = 'none' } = decoded; + + // We need this, since non-admin credentials are required to have an user id + let nonAdminCreds = {}; + + if (role !== 'admin') { + const permissions = await getPermissionsForUser(userId); + + if (R.isEmpty(permissions)) { + throw new Error(`No permissions for user id ${userId}.`); + } + + nonAdminCreds = { + userId, + // Read and write permissions + permissions, + }; + } + + return { + role, + permissions: {}, + ...nonAdminCreds, + }; +}; + module.exports = { getPermissionsForUser, splitCommaSeparatedPermissions, + getCredentialsForLegacyToken, + getCredentialsForKeycloakToken, }; diff --git a/yarn.lock b/yarn.lock index 437ae9fd62..dbc84ee379 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,7 +2363,7 @@ aws4@^1.2.1, aws4@^1.6.0, aws4@^1.8.0: axios@^0.18.0: version "0.18.0" - resolved "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= dependencies: follow-redirects "^1.3.0" @@ -2623,11 +2623,6 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" integrity sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w== -base64url@2.0.0, base64url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" - integrity sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs= - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -4180,12 +4175,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" - integrity sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE= +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + integrity sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM= dependencies: - base64url "^2.0.0" safe-buffer "^5.0.1" ee-first@1.1.1: @@ -7099,11 +7093,11 @@ jsonpointer@^4.0.0: integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= jsonwebtoken@^8.0.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz#690ec3a9e7e95e2884347ce3e9eb9d389aa598b3" - integrity sha512-1Wxh8ADP3cNyPl8tZ95WtraHXCAyXupgc0AhMHjU9er98BV+UcKsO7OJUjfhIu0Uba9A40n1oSx8dbJYrm+EoQ== + version "8.4.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz#8757f7b4cb7440d86d5e2f3becefa70536c8e46a" + integrity sha512-coyXjRTCy0pw5WYBpMvWOMN+Kjaik2MwTUIq9cna/W7NpO9E+iYbumZONAz3hcr+tXFJECoQVrtmIoC3Oz0gvg== dependencies: - jws "^3.1.4" + jws "^3.1.5" lodash.includes "^4.3.0" lodash.isboolean "^3.0.3" lodash.isinteger "^4.0.4" @@ -7112,7 +7106,6 @@ jsonwebtoken@^8.0.1: lodash.isstring "^4.0.1" lodash.once "^4.0.0" ms "^2.1.1" - xtend "^4.0.1" jsprim@^1.2.2: version "1.4.1" @@ -7129,14 +7122,13 @@ junk@^1.0.1: resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592" integrity sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI= -jwa@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" - integrity sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU= +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + integrity sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw== dependencies: - base64url "2.0.0" buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.9" + ecdsa-sig-formatter "1.0.10" safe-buffer "^5.0.1" jwk-to-pem@^2.0.0: @@ -7148,13 +7140,12 @@ jwk-to-pem@^2.0.0: elliptic "^6.2.3" safe-buffer "^5.0.1" -jws@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" - integrity sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI= +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + integrity sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ== dependencies: - base64url "^2.0.0" - jwa "^1.1.4" + jwa "^1.1.5" safe-buffer "^5.0.1" keycloak-admin@^1.8.0: @@ -7171,13 +7162,6 @@ keycloak-admin@^1.8.0: url-join "^4.0.0" url-template "^2.0.8" -keycloak-connect@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/keycloak-connect/-/keycloak-connect-4.5.0.tgz#2c7cbe27f018d2222a07ca62b03e75c0295dd3cb" - integrity sha512-bJhA9kqO2ENSoRdDWscXPNsmT8i0pRjt3GB6NWS/05cwxB8Q2qxWUTzLx7hFG0tPUWrY9vYd+TJVrHJru0Lkyg== - dependencies: - jwk-to-pem "^2.0.0" - keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -11999,7 +11983,7 @@ xregexp@4.0.0: resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= From c776c0e61b478ebad2fa233df23dd029918fd02b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:36 -0600 Subject: [PATCH 07/16] Add api subscription for backup/restore data --- services/api/package.json | 1 + services/api/src/clients/pubSub.js | 38 ++++++++++++++++ services/api/src/resolvers.js | 4 ++ services/api/src/resources/backup/events.js | 7 +++ .../api/src/resources/backup/resolvers.js | 45 ++++++++++++++++--- services/api/src/resources/index.js | 6 ++- services/api/src/typeDefs.js | 4 ++ 7 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 services/api/src/clients/pubSub.js create mode 100644 services/api/src/resources/backup/events.js diff --git a/services/api/package.json b/services/api/package.json index ab81bcc4e8..fb99feba24 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -35,6 +35,7 @@ "graphql": "^0.13.2", "graphql-iso-date": "^3.5.0", "graphql-list-fields": "^2.0.2", + "graphql-subscriptions": "^1.0.0", "graphql-tools": "^2.5.0", "jsonwebtoken": "^8.0.1", "jwk-to-pem": "^2.0.0", diff --git a/services/api/src/clients/pubSub.js b/services/api/src/clients/pubSub.js new file mode 100644 index 0000000000..4a9ff1bdac --- /dev/null +++ b/services/api/src/clients/pubSub.js @@ -0,0 +1,38 @@ +// @flow + +const R = require('ramda'); +const { PubSub, withFilter } = require('graphql-subscriptions'); +const { ForbiddenError } = require('apollo-server-express'); + +const pubSub = new PubSub(); + +const createProjectFilteredSubscriber = (events: string[], filterFn: any) => { + return { + // Allow publish functions to pass data without knowledge of query schema. + resolve: (payload: Object) => payload, + subscribe: withFilter( + ( + root, + { project }, + { + credentials: { + role, + permissions: { projects }, + }, + }, + ) => { + if (role !== 'admin' && !R.contains(String(project), projects)) { + throw new ForbiddenError(`No access to project ${project}.`); + } + + return pubSub.asyncIterator(events); + }, + filterFn, + ), + }; +}; + +module.exports = { + pubSub, + createProjectFilteredSubscriber, +}; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 27ca69273e..4e6c01c3d8 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -120,6 +120,7 @@ const { addRestore, getRestoreByBackupId, updateRestore, + backupSubscriber, } = require('./resources/backup/resolvers'); const { @@ -261,6 +262,9 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { updateTask, setEnvironmentServices, }, + Subscription: { + backupChanged: backupSubscriber, + }, Date: GraphQLDate, }; diff --git a/services/api/src/resources/backup/events.js b/services/api/src/resources/backup/events.js new file mode 100644 index 0000000000..63a57b0275 --- /dev/null +++ b/services/api/src/resources/backup/events.js @@ -0,0 +1,7 @@ +module.exports = { + BACKUP: { + ADDED: 'api.event.backup.added', + UPDATED: 'api.event.backup.updated', + DELETED: 'api.event.backup.deleted', + } +}; diff --git a/services/api/src/resources/backup/resolvers.js b/services/api/src/resources/backup/resolvers.js index 16495e79b1..699512174f 100644 --- a/services/api/src/resources/backup/resolvers.js +++ b/services/api/src/resources/backup/resolvers.js @@ -4,10 +4,12 @@ const R = require('ramda'); const { sendToLagoonLogs } = require('@lagoon/commons/src/logs'); const { createMiscTask } = require('@lagoon/commons/src/tasks'); const { query, isPatchEmpty } = require('../../util/db'); +const { pubSub, createProjectFilteredSubscriber } = require('../../clients/pubSub'); const sqlClient = require('../../clients/sqlClient'); const Sql = require('./sql'); const projectSql = require('../project/sql'); const environmentSql = require('../environment/sql'); +const EVENTS = require('./events'); /* :: @@ -60,7 +62,11 @@ const addBackup = async ( }), ); const rows = await query(sqlClient, Sql.selectBackup(insertId)); - return R.prop(0, rows); + const backup = R.prop(0, rows); + + pubSub.publish(EVENTS.BACKUP.ADDED, backup); + + return backup; }; const deleteBackup = async ( @@ -86,6 +92,9 @@ const deleteBackup = async ( await query(sqlClient, Sql.deleteBackup(backupId)); + const rows = await query(sqlClient, Sql.selectBackupByBackupId(backupId)); + pubSub.publish(EVENTS.BACKUP.DELETED, R.prop(0, rows)); + return 'success'; }; @@ -129,14 +138,16 @@ const addRestore = async ( let rows = await query(sqlClient, Sql.selectRestore(insertId)); const restoreData = R.prop(0, rows); + rows = await query(sqlClient, Sql.selectBackupByBackupId(backupId)); + const backupData = R.prop(0, rows); + + pubSub.publish(EVENTS.BACKUP.UPDATED, backupData); + // Allow creating restore data w/o executing the restore if (role === 'admin' && execute === false) { return restoreData; } - rows = await query(sqlClient, Sql.selectBackupByBackupId(backupId)); - const backupData = R.prop(0, rows); - rows = await query(sqlClient, environmentSql.selectEnvironmentById(backupData.environment)); const environmentData = R.prop(0, rows); @@ -229,9 +240,15 @@ const updateRestore = async ( }), ); - const rows = await query(sqlClient, Sql.selectRestoreByBackupId(backupId)); + let rows = await query(sqlClient, Sql.selectRestoreByBackupId(backupId)); + const restoreData = R.prop(0, rows); - return R.prop(0, rows); + rows = await query(sqlClient, Sql.selectBackupByBackupId(backupId)); + const backupData = R.prop(0, rows); + + pubSub.publish(EVENTS.BACKUP.UPDATED, backupData); + + return restoreData; }; // Data protected by environment auth @@ -245,6 +262,21 @@ const getRestoreByBackupId = async ( return R.prop(0, rows); }; +const backupSubscriber = createProjectFilteredSubscriber( + [ + EVENTS.BACKUP.ADDED, + EVENTS.BACKUP.UPDATED, + EVENTS.BACKUP.DELETED, + ], + async ( + payload, + { project }, + ) => { + const rows = await query(sqlClient, Sql.selectPermsForBackup(payload.backupId)); + return R.path(['0', 'pid'], rows) === `${project}`; + } +); + const Resolvers /* : ResolversObj */ = { addBackup, getBackupsByEnvironmentId, @@ -253,6 +285,7 @@ const Resolvers /* : ResolversObj */ = { addRestore, getRestoreByBackupId, updateRestore, + backupSubscriber, }; module.exports = Resolvers; diff --git a/services/api/src/resources/index.js b/services/api/src/resources/index.js index 366e53a0f0..635b17ad73 100644 --- a/services/api/src/resources/index.js +++ b/services/api/src/resources/index.js @@ -53,8 +53,12 @@ type ResolverFn = ( |}, ) => any; +type SubscribeObj = { + subscribe: () => mixed, +}; + export type ResolversObj = { - [string]: ResolverFn + [string]: ResolverFn | SubscribeObj }; type SqlFn = (...args: Array) => string; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index f6fc7bc779..66a2ec09db 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -912,6 +912,10 @@ const typeDefs = gql` updateTask(input: UpdateTaskInput): Task setEnvironmentServices(input: SetEnvironmentServicesInput!): [EnvironmentService] } + + type Subscription { + backupChanged(project: Int!): Backup + } `; module.exports = typeDefs; From 74f4f6aa52d08367f2c55a5a54647399ffee4354 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:37 -0600 Subject: [PATCH 08/16] UI now updates backups page in real-time when websockets are available --- .../api/src/resources/backup/resolvers.js | 5 +- services/ui/package.json | 1 + .../src/components/RestoreButton/Prepare.js | 2 +- .../ui/src/components/RestoreButton/index.js | 2 +- services/ui/src/lib/ApiConnection.js | 35 +++++++-- services/ui/src/pages/backups.js | 77 +++++++++++++++++-- yarn.lock | 22 ++++++ 7 files changed, 127 insertions(+), 17 deletions(-) diff --git a/services/api/src/resources/backup/resolvers.js b/services/api/src/resources/backup/resolvers.js index 699512174f..ca93b81f76 100644 --- a/services/api/src/resources/backup/resolvers.js +++ b/services/api/src/resources/backup/resolvers.js @@ -38,7 +38,10 @@ const getBackupsByEnvironmentId = async ( sqlClient, Sql.selectBackupsByEnvironmentId({ environmentId, includeDeleted }), ); - return rows; + + const newestFirst = R.sort(R.descend(R.prop('created')), rows); + + return newestFirst; }; const addBackup = async ( diff --git a/services/ui/package.json b/services/ui/package.json index fc06d899fc..d60f4996f3 100644 --- a/services/ui/package.json +++ b/services/ui/package.json @@ -16,6 +16,7 @@ "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.1", "apollo-link-http": "^1.5.5", + "apollo-link-ws": "^1.0.10", "dotenv-extended": "^2.2.0", "git-url-parse": "^10.0.1", "graphql": "^0.13.2", diff --git a/services/ui/src/components/RestoreButton/Prepare.js b/services/ui/src/components/RestoreButton/Prepare.js index dbd5a345c6..c37cfb89f8 100644 --- a/services/ui/src/components/RestoreButton/Prepare.js +++ b/services/ui/src/components/RestoreButton/Prepare.js @@ -17,7 +17,7 @@ const Prepare = ({ backupId, className }) => ( return ; } - if (called) { + if (loading || called) { return ; } diff --git a/services/ui/src/components/RestoreButton/index.js b/services/ui/src/components/RestoreButton/index.js index be6e897488..de9a67fb48 100644 --- a/services/ui/src/components/RestoreButton/index.js +++ b/services/ui/src/components/RestoreButton/index.js @@ -1,7 +1,7 @@ import React from 'react'; import Prepare from './Prepare'; -const RestoreButton = ({ backup: { backupId }, backup: { restore }, className }) => { +const RestoreButton = ({ backup: { backupId, restore }, className }) => { if (!restore) return ; diff --git a/services/ui/src/lib/ApiConnection.js b/services/ui/src/lib/ApiConnection.js index cca15f7fae..c2f07f787d 100644 --- a/services/ui/src/lib/ApiConnection.js +++ b/services/ui/src/lib/ApiConnection.js @@ -3,8 +3,10 @@ import getConfig from 'next/config'; import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { HttpLink } from 'apollo-link-http'; +import { WebSocketLink } from 'apollo-link-ws'; import { onError } from 'apollo-link-error'; import { ApolloLink } from 'apollo-link'; +import { getMainDefinition } from 'apollo-utilities'; import { ApolloProvider } from 'react-apollo'; import { AuthContext } from './withAuth'; import NotAuthenticated from '../components/NotAuthenticated'; @@ -18,6 +20,32 @@ const ApiConnection = ({ children }) => return ; } + const httpLink = new HttpLink({ + uri: publicRuntimeConfig.GRAPHQL_API, + headers: { + authorization: `Bearer ${auth.apiToken}`, + }, + }) + + const wsLink = new WebSocketLink({ + uri: publicRuntimeConfig.GRAPHQL_API.replace(/https?/, 'ws'), + options: { + reconnect: true, + connectionParams: { + authToken: auth.apiToken, + }, + }, + }); + + const requestLink = ApolloLink.split( + ({ query }) => { + const { kind, operation } = getMainDefinition(query); + return kind === 'OperationDefinition' && operation === 'subscription'; + }, + wsLink, + httpLink + ); + const client = new ApolloClient({ link: ApolloLink.from([ onError(({ graphQLErrors, networkError }) => { @@ -29,12 +57,7 @@ const ApiConnection = ({ children }) => ); if (networkError) console.log(`[Network error]: ${networkError}`); }), - new HttpLink({ - uri: publicRuntimeConfig.GRAPHQL_API, - headers: { - authorization: `Bearer ${auth.apiToken}`, - }, - }) + requestLink ]), cache: new InMemoryCache() }); diff --git a/services/ui/src/pages/backups.js b/services/ui/src/pages/backups.js index e8d92ebdfa..8956bd09be 100644 --- a/services/ui/src/pages/backups.js +++ b/services/ui/src/pages/backups.js @@ -1,4 +1,5 @@ import React from 'react'; +import * as R from 'ramda'; import { withRouter } from 'next/router' import Link from 'next/link' import { Query } from 'react-apollo'; @@ -17,20 +18,15 @@ const query = gql` name openshiftProjectName project { - name - } - deployments { id name - status - started - remoteId } backups { id - source + source backupId created + deleted restore { id status @@ -41,13 +37,31 @@ const query = gql` } `; +const subscribe = gql` + subscription subscribeToBackups($project: Int!) { + backupChanged(project: $project) { + id + source + backupId + created + deleted + restore { + id + status + restoreLocation + } + } + } +`; + const PageBackups = withRouter((props) => { return ( - {({ loading, error, data }) => { + {({ loading, error, data, subscribeToMore }) => { if (loading) return null; if (error) return `Error!: ${error}`; + const environment = data.environmentByOpenshiftProjectName; const breadcrumbs = [ { @@ -63,6 +77,53 @@ const PageBackups = withRouter((props) => { query: { name: environment.openshiftProjectName } } ]; + + subscribeToMore({ + document: subscribe, + variables: { project: environment.project.id }, + updateQuery: (prevStore, { subscriptionData }) => { + if (!subscriptionData.data) return prevStore; + const prevBackups = prevStore.environmentByOpenshiftProjectName.backups; + const incomingBackup = subscriptionData.data.backupChanged; + const existingIndex = prevBackups.findIndex(prevBackup => prevBackup.id === incomingBackup.id); + let newBackups; + + // New backup. + if (existingIndex === -1) { + // Don't add new deleted backups. + if (incomingBackup.deleted !== '0000-00-00 00:00:00') { + return prevStore; + } + + newBackups = [ + incomingBackup, + ...prevBackups, + ]; + } + // Existing backup. + else { + // Updated backup + if (incomingBackup.deleted === '0000-00-00 00:00:00') { + newBackups = Object.assign([...prevBackups], {[existingIndex]: incomingBackup}); + } + // Deleted backup + else { + newBackups = R.remove(existingIndex, 1, prevBackups); + } + } + + const newStore = { + ...prevStore, + environmentByOpenshiftProjectName: { + ...prevStore.environmentByOpenshiftProjectName, + backups: newBackups, + }, + }; + + return newStore; + } + }); + return ( diff --git a/yarn.lock b/yarn.lock index dbc84ee379..b72093b59f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1997,6 +1997,13 @@ apollo-link-http@^1.5.5: apollo-link "^1.2.3" apollo-link-http-common "^0.2.5" +apollo-link-ws@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/apollo-link-ws/-/apollo-link-ws-1.0.10.tgz#9fb5489a36f5fcb0d139b6ada0eea979ecad3967" + integrity sha512-1Yx4iIUsWS8wuAdVJ2LF+LdIYAsqHSto8eShwJ/d2SovocsMCwN9hyS+JkaOPD/KHAkavTWzN6l3XwSOdOwevQ== + dependencies: + apollo-link "^1.2.4" + apollo-link@^1.0.0, apollo-link@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.2.tgz#54c84199b18ac1af8d63553a68ca389c05217a03" @@ -2023,6 +2030,14 @@ apollo-link@^1.2.3: apollo-utilities "^1.0.0" zen-observable-ts "^0.8.10" +apollo-link@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.4.tgz#ab4d21d2e428db848e88b5e8f4adc717b19c954b" + integrity sha512-B1z+9H2nTyWEhMXRFSnoZ1vSuAYP+V/EdUJvRx9uZ8yuIBZMm6reyVtr1n0BWlKeSFyPieKJy2RLzmITAAQAMQ== + dependencies: + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.11" + apollo-server-caching@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.2.1.tgz#7e67f8c8cac829e622b394f0fb82579cabbeadfd" @@ -12126,6 +12141,13 @@ zen-observable-ts@^0.8.10: dependencies: zen-observable "^0.8.0" +zen-observable-ts@^0.8.11: + version "0.8.11" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.11.tgz#d54a27cd17dc4b4bb6bd008e5c096af7fcb068a9" + integrity sha512-8bs7rgGV4kz5iTb9isudkuQjtWwPnQ8lXq6/T76vrepYZVMsDEv6BXaEA+DHdJSK3KVLduagi9jSpSAJ5NgKHw== + dependencies: + zen-observable "^0.8.0" + zen-observable-ts@^0.8.6: version "0.8.8" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.8.tgz#1a586dc204fa5632a88057f879500e0d2ba06869" From 7286ba97c6a2c766e8a4abe985211af2e6aeef22 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:37 -0600 Subject: [PATCH 09/16] API subscriptions should filter on environment, not project --- services/api/src/clients/pubSub.js | 50 +++++++++++-------- .../api/src/resources/backup/resolvers.js | 13 ++--- services/api/src/typeDefs.js | 2 +- services/ui/src/pages/backups.js | 6 +-- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/services/api/src/clients/pubSub.js b/services/api/src/clients/pubSub.js index 4a9ff1bdac..3535132cd5 100644 --- a/services/api/src/clients/pubSub.js +++ b/services/api/src/clients/pubSub.js @@ -3,36 +3,46 @@ const R = require('ramda'); const { PubSub, withFilter } = require('graphql-subscriptions'); const { ForbiddenError } = require('apollo-server-express'); +const sqlClient = require('./sqlClient'); +const { query } = require('../util/db'); +const environmentSql = require('../resources/environment/sql'); const pubSub = new PubSub(); -const createProjectFilteredSubscriber = (events: string[], filterFn: any) => { +const createEnvironmentFilteredSubscriber = (events: string[]) => { return { // Allow publish functions to pass data without knowledge of query schema. resolve: (payload: Object) => payload, - subscribe: withFilter( - ( - root, - { project }, - { - credentials: { - role, - permissions: { projects }, - }, + subscribe: async (rootValue: any, args: any, context: any, info: any) => { + const { environment } = args; + const { + credentials: { + role, + permissions: { projects }, }, - ) => { - if (role !== 'admin' && !R.contains(String(project), projects)) { - throw new ForbiddenError(`No access to project ${project}.`); - } - - return pubSub.asyncIterator(events); - }, - filterFn, - ), + } = context; + + const rows = await query(sqlClient, environmentSql.selectEnvironmentById(environment)); + const project = R.path([0, 'project'], rows); + + if (role !== 'admin' && !R.contains(String(project), projects)) { + throw new ForbiddenError(`No access to project ${project}.`); + } + + const filtered = withFilter( + () => pubSub.asyncIterator(events), + ( + payload, + variables, + ) => payload.environment === String(variables.environment), + ); + + return filtered(rootValue, args, context, info); + }, }; }; module.exports = { pubSub, - createProjectFilteredSubscriber, + createEnvironmentFilteredSubscriber, }; diff --git a/services/api/src/resources/backup/resolvers.js b/services/api/src/resources/backup/resolvers.js index ca93b81f76..b8ff86eee2 100644 --- a/services/api/src/resources/backup/resolvers.js +++ b/services/api/src/resources/backup/resolvers.js @@ -4,7 +4,7 @@ const R = require('ramda'); const { sendToLagoonLogs } = require('@lagoon/commons/src/logs'); const { createMiscTask } = require('@lagoon/commons/src/tasks'); const { query, isPatchEmpty } = require('../../util/db'); -const { pubSub, createProjectFilteredSubscriber } = require('../../clients/pubSub'); +const { pubSub, createEnvironmentFilteredSubscriber } = require('../../clients/pubSub'); const sqlClient = require('../../clients/sqlClient'); const Sql = require('./sql'); const projectSql = require('../project/sql'); @@ -265,19 +265,12 @@ const getRestoreByBackupId = async ( return R.prop(0, rows); }; -const backupSubscriber = createProjectFilteredSubscriber( +const backupSubscriber = createEnvironmentFilteredSubscriber( [ EVENTS.BACKUP.ADDED, EVENTS.BACKUP.UPDATED, EVENTS.BACKUP.DELETED, - ], - async ( - payload, - { project }, - ) => { - const rows = await query(sqlClient, Sql.selectPermsForBackup(payload.backupId)); - return R.path(['0', 'pid'], rows) === `${project}`; - } + ] ); const Resolvers /* : ResolversObj */ = { diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 66a2ec09db..1324872651 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -914,7 +914,7 @@ const typeDefs = gql` } type Subscription { - backupChanged(project: Int!): Backup + backupChanged(environment: Int!): Backup } `; diff --git a/services/ui/src/pages/backups.js b/services/ui/src/pages/backups.js index 8956bd09be..a201210e53 100644 --- a/services/ui/src/pages/backups.js +++ b/services/ui/src/pages/backups.js @@ -38,8 +38,8 @@ const query = gql` `; const subscribe = gql` - subscription subscribeToBackups($project: Int!) { - backupChanged(project: $project) { + subscription subscribeToBackups($environment: Int!) { + backupChanged(environment: $environment) { id source backupId @@ -80,7 +80,7 @@ const PageBackups = withRouter((props) => { subscribeToMore({ document: subscribe, - variables: { project: environment.project.id }, + variables: { environment: environment.id }, updateQuery: (prevStore, { subscriptionData }) => { if (!subscriptionData.data) return prevStore; const prevBackups = prevStore.environmentByOpenshiftProjectName.backups; From 52ef93be3a01b7d127e84b94f004a5c7bf995b91 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:37 -0600 Subject: [PATCH 10/16] Add deploymentChanged API subscription and utilize it in UI --- services/api/src/resolvers.js | 2 + .../api/src/resources/deployment/events.js | 6 +++ .../api/src/resources/deployment/resolvers.js | 22 ++++++-- services/api/src/resources/deployment/sql.js | 2 +- services/api/src/typeDefs.js | 1 + .../ui/src/components/Deployments/index.js | 52 ++++++++++++++++++- 6 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 services/api/src/resources/deployment/events.js diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 4e6c01c3d8..5e6e92f8f6 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -19,6 +19,7 @@ const { addDeployment, deleteDeployment, updateDeployment, + deploymentSubscriber, } = require('./resources/deployment/resolvers'); const { @@ -264,6 +265,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { }, Subscription: { backupChanged: backupSubscriber, + deploymentChanged: deploymentSubscriber, }, Date: GraphQLDate, }; diff --git a/services/api/src/resources/deployment/events.js b/services/api/src/resources/deployment/events.js new file mode 100644 index 0000000000..b7e15865ea --- /dev/null +++ b/services/api/src/resources/deployment/events.js @@ -0,0 +1,6 @@ +module.exports = { + DEPLOYMENT: { + ADDED: 'api.event.deployment.added', + UPDATED: 'api.event.deployment.updated', + } +}; diff --git a/services/api/src/resources/deployment/resolvers.js b/services/api/src/resources/deployment/resolvers.js index 40ca451b8b..d6cd5b405b 100644 --- a/services/api/src/resources/deployment/resolvers.js +++ b/services/api/src/resources/deployment/resolvers.js @@ -4,6 +4,7 @@ const R = require('ramda'); const getFieldNames = require('graphql-list-fields'); const esClient = require('../../clients/esClient'); const sqlClient = require('../../clients/sqlClient'); +const { pubSub, createEnvironmentFilteredSubscriber } = require('../../clients/pubSub'); const { knex, ifNotAdmin, @@ -13,6 +14,7 @@ const { isPatchEmpty, } = require('../../util/db'); const Sql = require('./sql'); +const EVENTS = require('./events'); /* :: @@ -94,10 +96,11 @@ const getDeploymentsByEnvironmentId = async ( ); const rows = await query(sqlClient, prep({ eid })); + const newestFirst = R.sort(R.descend(R.prop('created')), rows); const requestedFields = getFieldNames(info); - return rows.filter(row => { + return newestFirst.filter(row => { if (R.isNil(name) || R.isEmpty(name)) { return true; } @@ -207,8 +210,10 @@ const addDeployment = async ( ); const rows = await query(sqlClient, Sql.selectDeployment(insertId)); + const deployment = await injectBuildLog(R.prop(0, rows)); - return injectBuildLog(R.prop(0, rows)); + pubSub.publish(EVENTS.DEPLOYMENT.ADDED, deployment); + return deployment; }; const deleteDeployment = async ( @@ -312,16 +317,27 @@ const updateDeployment = async ( ); const rows = await query(sqlClient, Sql.selectDeployment(id)); + const deployment = await injectBuildLog(R.prop(0, rows)); - return injectBuildLog(R.prop(0, rows)); + pubSub.publish(EVENTS.DEPLOYMENT.UPDATED, deployment); + + return deployment; }; +const deploymentSubscriber = createEnvironmentFilteredSubscriber( + [ + EVENTS.DEPLOYMENT.ADDED, + EVENTS.DEPLOYMENT.UPDATED, + ] +); + const Resolvers /* : ResolversObj */ = { getDeploymentsByEnvironmentId, getDeploymentByRemoteId, addDeployment, deleteDeployment, updateDeployment, + deploymentSubscriber, }; module.exports = Resolvers; diff --git a/services/api/src/resources/deployment/sql.js b/services/api/src/resources/deployment/sql.js index 404e1052a8..9a13f94fc3 100644 --- a/services/api/src/resources/deployment/sql.js +++ b/services/api/src/resources/deployment/sql.js @@ -57,7 +57,7 @@ const Sql /* : SqlObj */ = { .update(patch) .toString(), selectPermsForDeployment: (id /* : number */) => - knex('devployment') + knex('deployment') .select({ pid: 'project.id', cid: 'project.customer' }) .join('environment', 'deployment.environment', '=', 'environment.id') .join('project', 'environment.project', '=', 'project.id') diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 1324872651..4ad5668ff4 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -915,6 +915,7 @@ const typeDefs = gql` type Subscription { backupChanged(environment: Int!): Backup + deploymentChanged(environment: Int!): Deployment } `; diff --git a/services/ui/src/components/Deployments/index.js b/services/ui/src/components/Deployments/index.js index e057df0bf3..bd0f76d8e9 100644 --- a/services/ui/src/components/Deployments/index.js +++ b/services/ui/src/components/Deployments/index.js @@ -22,6 +22,19 @@ const query = gql` } `; +const subscribe = gql` + subscription subscribeToDeployments($environment: Int!) { + deploymentChanged(environment: $environment) { + id + name + status + created + started + completed + } + } +`; + const getDuration = deployment => { const deploymentStart = deployment.started || deployment.created; const durationStart = @@ -45,7 +58,7 @@ const Deployments = ({ projectName }) => (
- {({ loading, error, data }) => { + {({ loading, error, data, subscribeToMore }) => { if (loading) { return
Loading...
; } @@ -54,6 +67,43 @@ const Deployments = ({ projectName }) => ( return
Error: {error.toString()}
; } + subscribeToMore({ + document: subscribe, + variables: { environment: R.path( + ['environmentByOpenshiftProjectName', 'id'], + data + )}, + updateQuery: (prevStore, { subscriptionData }) => { + if (!subscriptionData.data) return prevStore; + const prevDeployments = prevStore.environmentByOpenshiftProjectName.deployments; + const incomingDeployment = subscriptionData.data.deploymentChanged; + const existingIndex = prevDeployments.findIndex(prevDeployment => prevDeployment.id === incomingDeployment.id); + let newDeployments; + + // New deployment. + if (existingIndex === -1) { + newDeployments = [ + incomingDeployment, + ...prevDeployments, + ]; + } + // Updated deployment + else { + newDeployments = Object.assign([...prevDeployments], {[existingIndex]: incomingDeployment}); + } + + const newStore = { + ...prevStore, + environmentByOpenshiftProjectName: { + ...prevStore.environmentByOpenshiftProjectName, + deployments: newDeployments, + }, + }; + + return newStore; + } + }); + const deployments = R.path( ['environmentByOpenshiftProjectName', 'deployments'], data From 8ab8b831470dd5fc341ccf0bb1d6ea0f956abf56 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:37 -0600 Subject: [PATCH 11/16] Use rabbitmq for API pubsub engine --- services/api/package.json | 1 + services/api/src/clients/pubSub.js | 35 +++++++++++++++- yarn.lock | 67 +++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/services/api/package.json b/services/api/package.json index fb99feba24..d8d662aa62 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -35,6 +35,7 @@ "graphql": "^0.13.2", "graphql-iso-date": "^3.5.0", "graphql-list-fields": "^2.0.2", + "graphql-rabbitmq-subscriptions": "^1.1.0", "graphql-subscriptions": "^1.0.0", "graphql-tools": "^2.5.0", "jsonwebtoken": "^8.0.1", diff --git a/services/api/src/clients/pubSub.js b/services/api/src/clients/pubSub.js index 3535132cd5..558f4e9706 100644 --- a/services/api/src/clients/pubSub.js +++ b/services/api/src/clients/pubSub.js @@ -2,12 +2,45 @@ const R = require('ramda'); const { PubSub, withFilter } = require('graphql-subscriptions'); +const { AmqpPubSub } = require('graphql-rabbitmq-subscriptions'); const { ForbiddenError } = require('apollo-server-express'); +const logger = require('../logger'); const sqlClient = require('./sqlClient'); const { query } = require('../util/db'); const environmentSql = require('../resources/environment/sql'); -const pubSub = new PubSub(); +const rabbitmqHost = process.env.RABBITMQ_HOST || 'rabbitmq'; +const rabbitmqUsername = process.env.RABBITMQ_USERNAME || 'guest'; +const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || 'guest'; + +/* eslint-disable class-methods-use-this */ +class LoggerConverter { + child() { + return { + debug: logger.debug, + trace: logger.silly, + error: logger.error, + }; + } + + error(...args) { + return logger.error.apply(args); + } + + debug(...args) { + return logger.debug(args); + } + + trace(...args) { + return logger.silly(args); + } +} +/* eslint-enable class-methods-use-this */ + +const pubSub = new AmqpPubSub({ + config: `amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`, + logger: new LoggerConverter(), +}); const createEnvironmentFilteredSubscriber = (events: string[]) => { return { diff --git a/yarn.lock b/yarn.lock index b72093b59f..33c39ce405 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1557,11 +1557,24 @@ dependencies: "@types/node" "*" +"@types/amqplib@^0.5.1": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@types/amqplib/-/amqplib-0.5.9.tgz#94fa80fad2fdbe78f458ccb9c5fb0abfe62f817c" + integrity sha512-V4OFDKeu8rY2DnbHAh7J7DaJ8L8LZ7EyOLBoXsN2rtlCoB2QwaTgx2SsBd22mPUC9kl+vK7D+nfLq34jLhEjlA== + dependencies: + "@types/bluebird" "*" + "@types/node" "*" + "@types/async@2.0.50": version "2.0.50" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb" integrity sha512-VMhZMMQgV1zsR+lX/0IBfAk+8Eb7dPVMWiQGFAt3qjo5x7Ml6b77jUo0e1C3ToD+XRDXqtrfw+6AB0uUsPEr3Q== +"@types/bluebird@*": + version "3.5.24" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.24.tgz#11f76812531c14f793b8ecbf1de96f672905de8a" + integrity sha512-YeQoDpq4Lm8ppSBqAnAeF/xy1cYp/dMTif2JFcvmAbETMRlvKHT2iLcWu+WyYiJO3b3Ivokwo7EQca/xfLVJmg== + "@types/body-parser@*": version "1.16.8" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" @@ -1578,6 +1591,20 @@ "@types/connect" "*" "@types/node" "*" +"@types/bunyan@0.0.35": + version "0.0.35" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-0.0.35.tgz#7bab9f9e26c580413b8c323d3fa76bd08dd1bcac" + integrity sha1-e6ufnibFgEE7jDI9P6dr0I3RvKw= + dependencies: + "@types/node" "*" + +"@types/bunyan@^1.8.0": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.5.tgz#d992adbce8ed20cde634764bd8f269f29f703647" + integrity sha512-7n8ANtxh2c5A/NfCuv8cVtWcgSLdq76MQbtmbInpzXuPw4TSAReUJ+MGHK4m67I4zI3ynCJoABfaeHYJaYSeRg== + dependencies: + "@types/node" "*" + "@types/connect@*": version "3.4.32" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" @@ -1649,6 +1676,11 @@ resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.12.6.tgz#3d619198585fcabe5f4e1adfb5cf5f3388c66c13" integrity sha512-wXAVyLfkG1UMkKOdMijVWFky39+OD/41KftzqfX1Oejd0Gm6dOIKjCihSVECg6X7PHjftxXmfOKA/d1H79ZfvQ== +"@types/graphql@^0.9.1": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.4.tgz#cdeb6bcbef9b6c584374b81aa7f48ecf3da404fa" + integrity sha512-ob2dps4itT/Le5DbxjssBXtBnloDIRUbkgtAvaB42mJ8pVIWMRuURD9WjnhaEGZ4Ql/EryXMQWeU8Y0EU73QLw== + "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" @@ -4369,6 +4401,11 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" +es6-promise@^4.0.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + es6-promise@^4.1.1: version "4.2.4" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" @@ -5581,6 +5618,25 @@ graphql-list-fields@^2.0.2: resolved "https://registry.yarnpkg.com/graphql-list-fields/-/graphql-list-fields-2.0.2.tgz#a4ade3cfa2028a2ac32d3f2870f5f8c5b5d5b466" integrity sha512-9TSAwcVA3KWw7JWYep5NCk2aw3wl1ayLtbMpmG7l26vh1FZ+gZexNPP+XJfUFyJa71UU0zcKSgtgpsrsA3Xv9Q== +graphql-rabbitmq-subscriptions@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/graphql-rabbitmq-subscriptions/-/graphql-rabbitmq-subscriptions-1.1.0.tgz#43a8de9f8a6ab8e8d48e60d0c8b8bc6e6ec16bc2" + integrity sha512-PK9hkh4O7E+VuK9K7jwLbYaaDpqMmoSLOYEoKWMaqrt3yIGn+kIccKNnzQHyRs5zeCazgPhQ0BCyYj038KailA== + dependencies: + "@types/bunyan" "^1.8.0" + async "^2.5.0" + graphql-subscriptions "^0.4.4" + rabbitmq-pub-sub "^0.2.5" + +graphql-subscriptions@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz#39cff32d08dd3c990113864bab77154403727e9b" + integrity sha512-hqfUsZv39qmK4SEoKMnTO05U4EVvIeAD4ai5ztE9gCl4hEdeaF2Q5gvF80ONQQAnkys4odzxWYd2tBLS/cWl8g== + dependencies: + "@types/graphql" "^0.9.1" + es6-promise "^4.0.5" + iterall "^1.1.1" + graphql-subscriptions@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8" @@ -6635,7 +6691,7 @@ istanbul-reports@^1.5.1: dependencies: handlebars "^4.0.3" -iterall@^1.1.3, iterall@^1.2.1: +iterall@^1.1.1, iterall@^1.1.3, iterall@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== @@ -9366,6 +9422,15 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +rabbitmq-pub-sub@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/rabbitmq-pub-sub/-/rabbitmq-pub-sub-0.2.5.tgz#87ebfdf34eed72b763afd969c3ce47c576823129" + integrity sha1-h+v9807tcrdjr9lpw85HxXaCMSk= + dependencies: + "@types/amqplib" "^0.5.1" + "@types/bunyan" "0.0.35" + amqplib "^0.5.1" + raf@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" From 3597c24293cee43cfa4383c5c67278ef6cd34309 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 10 Dec 2018 10:31:37 -0600 Subject: [PATCH 12/16] Add taskChanged API subscription and utilize it in UI --- services/api/src/resolvers.js | 2 + services/api/src/resources/task/events.js | 6 +++ services/api/src/resources/task/resolvers.js | 27 ++++++++-- services/api/src/typeDefs.js | 1 + services/ui/src/pages/tasks.js | 53 +++++++++++++++++++- 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 services/api/src/resources/task/events.js diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 5e6e92f8f6..c7c9d07c35 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -28,6 +28,7 @@ const { addTask, deleteTask, updateTask, + taskSubscriber, } = require('./resources/task/resolvers'); const { @@ -266,6 +267,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { Subscription: { backupChanged: backupSubscriber, deploymentChanged: deploymentSubscriber, + taskChanged: taskSubscriber, }, Date: GraphQLDate, }; diff --git a/services/api/src/resources/task/events.js b/services/api/src/resources/task/events.js new file mode 100644 index 0000000000..67760f5732 --- /dev/null +++ b/services/api/src/resources/task/events.js @@ -0,0 +1,6 @@ +module.exports = { + TASK: { + ADDED: 'api.event.task.added', + UPDATED: 'api.event.task.updated', + } +}; diff --git a/services/api/src/resources/task/resolvers.js b/services/api/src/resources/task/resolvers.js index 4d4738de9a..49872a51cb 100644 --- a/services/api/src/resources/task/resolvers.js +++ b/services/api/src/resources/task/resolvers.js @@ -3,6 +3,7 @@ const R = require('ramda'); const { sendToLagoonLogs } = require('@lagoon/commons/src/logs'); const { createTaskTask } = require('@lagoon/commons/src/tasks'); +const { pubSub, createEnvironmentFilteredSubscriber } = require('../../clients/pubSub'); const esClient = require('../../clients/esClient'); const sqlClient = require('../../clients/sqlClient'); const { @@ -16,6 +17,7 @@ const { const Sql = require('./sql'); const projectSql = require('../project/sql'); const environmentSql = require('../environment/sql'); +const EVENTS = require('./events'); /* :: @@ -93,7 +95,9 @@ const getTasksByEnvironmentId = async ( const rows = await query(sqlClient, prep({ eid })); - return rows.map(row => injectLogs(row)); + const newestFirst = R.sort(R.descend(R.prop('created')), rows); + + return newestFirst.map(row => injectLogs(row)); }; const getTaskByRemoteId = async ( @@ -190,11 +194,13 @@ const addTask = async ( ); let rows = await query(sqlClient, Sql.selectTask(insertId)); - const taskData = R.prop(0, rows); + const taskData = await injectLogs(R.prop(0, rows)); + + pubSub.publish(EVENTS.TASK.ADDED, taskData); // Allow creating task data w/o executing the task if (role === 'admin' && execute === false) { - return injectLogs(taskData); + return taskData; } rows = await query(sqlClient, environmentSql.selectEnvironmentById(taskData.environment)); @@ -216,7 +222,7 @@ const addTask = async ( ); } - return injectLogs(taskData); + return taskData; }; const deleteTask = async ( @@ -321,16 +327,27 @@ const updateTask = async ( ); const rows = await query(sqlClient, Sql.selectTask(id)); + const taskData = await injectLogs(R.prop(0, rows)); - return injectLogs(R.prop(0, rows)); + pubSub.publish(EVENTS.TASK.UPDATED, taskData); + + return taskData; }; +const taskSubscriber = createEnvironmentFilteredSubscriber( + [ + EVENTS.TASK.ADDED, + EVENTS.TASK.UPDATED, + ] +); + const Resolvers /* : ResolversObj */ = { getTasksByEnvironmentId, getTaskByRemoteId, addTask, deleteTask, updateTask, + taskSubscriber, }; module.exports = Resolvers; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 4ad5668ff4..be89a505df 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -916,6 +916,7 @@ const typeDefs = gql` type Subscription { backupChanged(environment: Int!): Backup deploymentChanged(environment: Int!): Deployment + taskChanged(environment: Int!): Task } `; diff --git a/services/ui/src/pages/tasks.js b/services/ui/src/pages/tasks.js index 02d9dadab4..cc346db6ef 100644 --- a/services/ui/src/pages/tasks.js +++ b/services/ui/src/pages/tasks.js @@ -43,6 +43,23 @@ const query = gql` } `; +const subscribe = gql` + subscription subscribeToTasks($environment: Int!) { + taskChanged(environment: $environment) { + id + name + status + created + started + completed + remoteId + command + service + logs + } + } +`; + const PageTasks = withRouter(props => { return ( @@ -50,7 +67,7 @@ const PageTasks = withRouter(props => { query={query} variables={{ openshiftProjectName: props.router.query.name }} > - {({ loading, error, data }) => { + {({ loading, error, data, subscribeToMore }) => { if (loading) return null; if (error) return `Error!: ${error}`; const environment = data.environmentByOpenshiftProjectName; @@ -69,6 +86,40 @@ const PageTasks = withRouter(props => { } ]; + subscribeToMore({ + document: subscribe, + variables: { environment: environment.id}, + updateQuery: (prevStore, { subscriptionData }) => { + if (!subscriptionData.data) return prevStore; + const prevTasks = prevStore.environmentByOpenshiftProjectName.tasks; + const incomingTask = subscriptionData.data.taskChanged; + const existingIndex = prevTasks.findIndex(prevTask => prevTask.id === incomingTask.id); + let newTasks; + + // New task. + if (existingIndex === -1) { + newTasks = [ + incomingTask, + ...prevTasks, + ]; + } + // Updated task + else { + newTasks = Object.assign([...prevTasks], {[existingIndex]: incomingTask}); + } + + const newStore = { + ...prevStore, + environmentByOpenshiftProjectName: { + ...prevStore.environmentByOpenshiftProjectName, + tasks: newTasks, + }, + }; + + return newStore; + } + }); + const tasks = environment.tasks.map(task => { const taskStart = task.started || task.created; From 1b5b2aaafcd464953845904257ce4de0676ed829 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Dec 2018 21:55:15 -0600 Subject: [PATCH 13/16] Add file upload/download for tasks to api --- .../docker-entrypoint-initdb.d/00-tables.sql | 14 ++ services/api/package.json | 1 + services/api/src/clients/aws.js | 22 +++ services/api/src/resolvers.js | 9 ++ services/api/src/resources/file/resolvers.js | 152 ++++++++++++++++++ services/api/src/resources/file/sql.js | 69 ++++++++ services/api/src/typeDefs.js | 19 +++ yarn.lock | 32 +++- 8 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 services/api/src/clients/aws.js create mode 100644 services/api/src/resources/file/resolvers.js create mode 100644 services/api/src/resources/file/sql.js diff --git a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql index 504abf5cd6..8ad9133bed 100644 --- a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql +++ b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql @@ -160,6 +160,14 @@ CREATE TABLE IF NOT EXISTS task ( remote_id varchar(50) NULL ); +CREATE TABLE IF NOT EXISTS s3_file ( + id int NOT NULL auto_increment PRIMARY KEY, + filename varchar(100) NOT NULL, + s3_key text NOT NULL, + created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted datetime NOT NULL DEFAULT '0000-00-00 00:00:00' +); + -- Junction Tables CREATE TABLE IF NOT EXISTS project_notification ( @@ -186,3 +194,9 @@ CREATE TABLE IF NOT EXISTS project_user ( usid int REFERENCES user (id), CONSTRAINT project_user_pkey PRIMARY KEY (pid, usid) ); + +CREATE TABLE IF NOT EXISTS task_file ( + tid int REFERENCES task (id), + fid int REFERENCES file (id), + CONSTRAINT task_file_pkey PRIMARY KEY (tid, fid) +); diff --git a/services/api/package.json b/services/api/package.json index d8d662aa62..92407457cc 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -20,6 +20,7 @@ "dependencies": { "@lagoon/commons": "4.0.0", "apollo-server-express": "^2.2.5", + "aws-sdk": "^2.378.0", "axios": "^0.18.0", "body-parser": "^1.18.2", "camelcase-keys": "^4.2.0", diff --git a/services/api/src/clients/aws.js b/services/api/src/clients/aws.js new file mode 100644 index 0000000000..5d42302ae5 --- /dev/null +++ b/services/api/src/clients/aws.js @@ -0,0 +1,22 @@ +// @flow + +const R = require('ramda'); +const AWS = require('aws-sdk'); + +const s3Host = R.propOr('http://docker.for.mac.localhost:9000', 'S3_HOST', process.env); +const accessKeyId = R.propOr('minio', 'S3_ACCESS_KEY_ID', process.env); +const secretAccessKey = R.propOr('minio123', 'S3_SECRET_ACCESS_KEY', process.env); +const bucket = R.propOr('api-files', 'S3_BUCKET', process.env); + +const s3 = new AWS.S3({ + endpoint: s3Host, + accessKeyId, + secretAccessKey, + s3ForcePathStyle: true, + signatureVersion: 'v4', +}); + +module.exports = { + s3Client: s3, + s3Bucket: bucket, +}; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 67eb23b41b..3aa7671d25 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -34,6 +34,12 @@ const { taskSubscriber, } = require('./resources/task/resolvers'); +const { + getFilesByTaskId, + uploadFilesForTask, + deleteFilesForTask, +} = require('./resources/file/resolvers'); + const { addOrUpdateEnvironment, addOrUpdateEnvironmentStorage, @@ -167,6 +173,7 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { }, Task: { environment: getEnvironmentByTaskId, + files: getFilesByTaskId, }, Notification: { __resolveType(obj) { @@ -269,6 +276,8 @@ const resolvers /* : { [string]: ResolversObj | typeof GraphQLDate } */ = { deleteTask, updateTask, setEnvironmentServices, + uploadFilesForTask, + deleteFilesForTask, }, Subscription: { backupChanged: backupSubscriber, diff --git a/services/api/src/resources/file/resolvers.js b/services/api/src/resources/file/resolvers.js new file mode 100644 index 0000000000..7d8446dd26 --- /dev/null +++ b/services/api/src/resources/file/resolvers.js @@ -0,0 +1,152 @@ +// @flow + +const R = require('ramda'); +const sqlClient = require('../../clients/sqlClient'); +const { s3Client, s3Bucket } = require('../../clients/aws'); +const { query } = require('../../util/db'); +const Sql = require('./sql'); +const taskSql = require('../task/sql'); + +/* :: + +import type {ResolversObj} from '../'; + +*/ + +const generateDownloadLink = file => { + const url = s3Client.getSignedUrl('getObject', { + Bucket: s3Bucket, + Key: file.s3Key, + Expires: 900, // 15 minutes + }); + + return { + ...file, + download: url, + }; +}; + +const fileIsDeleted = file => file.deleted !== '0000-00-00 00:00:00'; + +const getFilesByTaskId = async ( + { id: tid }, + args, + { + credentials: { + role, + permissions: { customers, projects }, + }, + }, +) => { + if (role !== 'admin') { + const rowsPerms = await query(sqlClient, taskSql.selectPermsForTask(tid)); + + if ( + !R.contains(R.path(['0', 'pid'], rowsPerms), projects) && + !R.contains(R.path(['0', 'cid'], rowsPerms), customers) + ) { + throw new Error('Unauthorized.'); + } + } + + const rows = await query(sqlClient, Sql.selectTaskFiles(tid)); + + return R.pipe( + R.sort(R.descend(R.prop('created'))), + R.reject(fileIsDeleted), + R.map(generateDownloadLink), + )(rows); +}; + +const uploadFilesForTask = async ( + root, + { input: { task, files } }, + { + credentials: { + role, + permissions: { customers, projects }, + }, + }, +) => { + if (role !== 'admin') { + const rowsPerms = await query(sqlClient, taskSql.selectPermsForTask(task)); + + if ( + !R.contains(R.path(['0', 'pid'], rowsPerms), projects) && + !R.contains(R.path(['0', 'cid'], rowsPerms), customers) + ) { + throw new Error('Unauthorized.'); + } + } + + const resolvedFiles = await Promise.all(files); + const uploadAndTrackFiles = resolvedFiles.map(async newFile => { + const s3_key = `tasks/${task}/${newFile.filename}`; + const params = { + Bucket: s3Bucket, + Key: s3_key, + Body: newFile.stream, + ACL: 'private', + }; + await s3Client.upload(params).promise(); + + const { + info: { insertId }, + } = await query( + sqlClient, + Sql.insertFile({ + filename: newFile.filename, + s3_key, + created: '2010-01-01 00:00:00', + }), + ); + + await query( + sqlClient, + Sql.insertFileTask({ + tid: task, + fid: insertId, + }), + ); + }); + + await Promise.all(uploadAndTrackFiles); + + const rows = await query(sqlClient, taskSql.selectTask(task)); + + return R.prop(0, rows); +}; + +const deleteFilesForTask = async ( + root, + { input: { id } }, + { credentials: { role } }, +) => { + if (role !== 'admin') { + throw new Error('Unauthorized.'); + } + + const rows = await query(sqlClient, Sql.selectTaskFiles(id)); + const deleteObjects = R.map(file => ({ Key: file.s3Key }), rows); + + const params = { + Bucket: s3Bucket, + Delete: { + Objects: deleteObjects, + Quiet: false, + }, + }; + await s3Client.deleteObjects(params).promise(); + + await query(sqlClient, Sql.deleteFileTask(id)); + + return 'success'; +}; + +const Resolvers /* : ResolversObj */ = { + getFilesByTaskId, + uploadFilesForTask, + deleteFilesForTask, +}; + +module.exports = Resolvers; diff --git a/services/api/src/resources/file/sql.js b/services/api/src/resources/file/sql.js new file mode 100644 index 0000000000..ddf6f0b43d --- /dev/null +++ b/services/api/src/resources/file/sql.js @@ -0,0 +1,69 @@ +// @flow + +const { knex } = require('../../util/db'); + +/* :: + +import type {SqlObj} from '../'; + +*/ + +const Sql /* : SqlObj */ = { + selectFile: (id /* : number */) => + knex('s3_file') + .where('s3_file.id', '=', id) + .toString(), + selectTaskFiles: (tid /* : number */) => + knex('task_file') + .where('task_file.tid', '=', tid) + .join('s3_file', 'task_file.fid', '=', 's3_file.id') + .toString(), + insertFile: ( + { + id, + filename, + s3_key, + created, + } /* : { + id: number, + filename: string, + s3_key: string, + created: number, + } */, + ) => + knex('s3_file') + .insert({ + id, + filename, + s3_key, + created, + }) + .toString(), + insertFileTask: ( + { + tid, + fid, + } /* : { + id: number, + tid: number, + fid: number, + } */, + ) => + knex('task_file') + .insert({ + tid, + fid, + }) + .toString(), + deleteFileTask: (id /* : number */) => + knex('s3_file') + .join('task_file', 's3_file.id', '=', 'task_file.fid') + .where('task_file.tid', id) + .andWhere('s3_file.deleted', '0000-00-00 00:00:00') + .update({ + 's3_file.deleted': knex.fn.now(), + }) + .toString(), +}; + +module.exports = Sql; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index d99bc74181..79d7ba22cd 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -65,6 +65,13 @@ const typeDefs = gql` FAILED } + type File { + id: Int + filename: String + download: String + created: String + } + type SshKey { id: Int name: String @@ -408,6 +415,7 @@ const typeDefs = gql` command: String remoteId: String logs: String + files: [File] } input DeleteEnvironmentInput { @@ -828,6 +836,15 @@ const typeDefs = gql` services: [String]! } + input UploadFilesForTaskInput { + task: Int!, + files: [Upload]!, + } + + input DeleteFilesForTaskInput { + id: Int! + } + type Mutation { addCustomer(input: AddCustomerInput!): Customer updateCustomer(input: UpdateCustomerInput!): Customer @@ -920,6 +937,8 @@ const typeDefs = gql` deleteTask(input: DeleteTaskInput!): String updateTask(input: UpdateTaskInput): Task setEnvironmentServices(input: SetEnvironmentServicesInput!): [EnvironmentService] + uploadFilesForTask(input: UploadFilesForTaskInput!): Task + deleteFilesForTask(input: DeleteFilesForTaskInput!): String } type Subscription { diff --git a/yarn.lock b/yarn.lock index 33c39ce405..fa8a008a3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2388,6 +2388,21 @@ aws-sdk@^2.130.0: xml2js "0.4.17" xmlbuilder "4.2.1" +aws-sdk@^2.378.0: + version "2.378.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.378.0.tgz#8b0f9c36c7dc2f459138695ef39a274a08de2cb2" + integrity sha512-N4/DfgNLOeALgU0Lp7dCyuk1HDtP1eLVurj8dJbY6KyuNF3izMzLzb6wlx9UNHiczzJ2pT9XiSatHG0ZeOsDrQ== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.8" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.19" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -4759,7 +4774,7 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== -events@^1.0.0, events@^1.1.1: +events@1.1.1, events@^1.0.0, events@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= @@ -6029,7 +6044,7 @@ iconv-lite@0.4.23, iconv-lite@^0.4.22, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@^1.1.4: +ieee754@1.1.8, ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q= @@ -12046,6 +12061,14 @@ xml2js@0.4.17: sax ">=0.6.0" xmlbuilder "^4.1.0" +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xmlbuilder@4.2.1, xmlbuilder@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" @@ -12058,6 +12081,11 @@ xmlbuilder@9.0.4: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" integrity sha1-UZy0ymhtAFqEINNJbz8MruzKWA8= +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xregexp@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" From 3e2a59eb26dbaf9b5a9fb8ee40bf70b86d55839c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Dec 2018 21:55:15 -0600 Subject: [PATCH 14/16] Upload files generated by taskDrushArchiveDump to api --- node-packages/commons/src/jwt.js | 6 +++ services/api/src/resources/task/resolvers.js | 16 ++++---- services/api/src/util/auth.js | 35 ++++++++++------ services/openshiftjobs/Dockerfile | 5 ++- services/openshiftjobs/src/index.js | 42 ++++++++++++++++++++ 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/node-packages/commons/src/jwt.js b/node-packages/commons/src/jwt.js index 3b57632d75..fff08532b7 100644 --- a/node-packages/commons/src/jwt.js +++ b/node-packages/commons/src/jwt.js @@ -5,12 +5,18 @@ const jwt = require('jsonwebtoken'); /* :: type Role = 'none' | 'admin' | 'drush'; +type Permissions = { + projects: number[], + customers: number[], +}; type Payload = {| userId: number, role: Role, + permissions: Permissions, + // Issuer - Information on who created this token iss: string, diff --git a/services/api/src/resources/task/resolvers.js b/services/api/src/resources/task/resolvers.js index 487d982377..aea05fc91c 100644 --- a/services/api/src/resources/task/resolvers.js +++ b/services/api/src/resources/task/resolvers.js @@ -262,15 +262,13 @@ const taskDrushArchiveDump = async ( await envValidators.userAccessEnvironment(credentials, environmentId); await envValidators.environmentHasService(environmentId, 'cli'); - const environment = await environmentHelpers.getEnvironmentById(environmentId); - const filename = `drupal-${Date.now()}-${Math.floor(Math.random() * 998) + 1}.tar.gz`; - const command = String.raw`drush status --format=yaml | \ -grep '%files' | \ -awk '{print $2}' | \ -xargs -I_path drush ard --pipe --destination=_path/private/${filename} | \ -xargs -I_file -- printf 'Your archive has been saved to _file.\nYou can download it by running "drush rsync @${ - environment.name - }:_file ./".'`; + const command = String.raw`drush ard --pipe | \ +xargs -I_file curl -sS "$TASK_API_HOST"/graphql \ +-H "Authorization: Bearer $TASK_API_AUTH" \ +-F operations='{ "query": "mutation ($task: Int!, $files: [Upload!]!) { uploadFilesForTask(input:{task:$task, files:$files}) { id files { filename } } }", "variables": { "task": '"$TASK_DATA_ID"', "files": [null] } }' \ +-F map='{ "0": ["variables.files.0"] }' \ +-F 0=@_file +`; const taskData = await Helpers.addTask({ name: 'Drush archive-dump', diff --git a/services/api/src/util/auth.js b/services/api/src/util/auth.js index 0649e221b5..e8d177fce0 100644 --- a/services/api/src/util/auth.js +++ b/services/api/src/util/auth.js @@ -159,30 +159,39 @@ const getCredentialsForLegacyToken = async token => { throw new Error(`Error decoding token: ${e.message}`); } - const { userId, role = 'none' } = decoded; + const { userId, permissions, role = 'none' } = decoded; - // We need this, since non-admin credentials are required to have an user id - let nonAdminCreds = {}; + if (role === 'admin') { + return { + role, + permissions: {}, + }; + } - if (role !== 'admin') { - const permissions = await getPermissionsForUser(userId); + // Get permissions for user, override any from JWT. + if (userId) { + const dbPermissions = await getPermissionsForUser(userId); - if (R.isEmpty(permissions)) { + if (R.isEmpty(dbPermissions)) { throw new Error(`No permissions for user id ${userId}.`); } - nonAdminCreds = { + return { userId, - // Read and write permissions + role, + permissions: dbPermissions, + }; + } + + // Use permissions from JWT. + if (permissions) { + return { + role, permissions, }; } - return { - role, - permissions: {}, - ...nonAdminCreds, - }; + throw new Error('Cannot authenticate non-admin user with no userId or permissions.'); }; module.exports = { diff --git a/services/openshiftjobs/Dockerfile b/services/openshiftjobs/Dockerfile index 5e75414532..cd1e5449a8 100644 --- a/services/openshiftjobs/Dockerfile +++ b/services/openshiftjobs/Dockerfile @@ -21,8 +21,9 @@ COPY . . # Verify that all dependencies have been installed via the yarn-workspace-builder RUN yarn check --verify-tree -# Making sure we run in production -ENV NODE_ENV production +ENV NODE_ENV production \ + JWTSECRET=super-secret-string \ + JWTAUDIENCE=api.dev RUN yarn run build diff --git a/services/openshiftjobs/src/index.js b/services/openshiftjobs/src/index.js index f5a56718e6..5639a11ba9 100644 --- a/services/openshiftjobs/src/index.js +++ b/services/openshiftjobs/src/index.js @@ -3,6 +3,7 @@ const promisify = require('util').promisify; const OpenShiftClient = require('openshift-client'); const R = require('ramda'); +const { createJWTWithoutUserId } = require('@lagoon/commons/src/jwt'); const { logger } = require('@lagoon/commons/src/local-logging'); const { getOpenShiftInfoForProject, @@ -18,6 +19,15 @@ const { createTaskMonitor } = require('@lagoon/commons/src/tasks'); +const lagoonApiRoute = R.compose( + // Default to the gateway IP in virtualbox, so pods running in minishift can + // connect to docker-for-mac containers. + R.defaultTo('http://10.0.2.2:3000'), + R.find(R.test(/api-/)), + R.split(','), + R.propOr('', 'LAGOON_ROUTES') +)(process.env); + initSendToLagoonLogs(); initSendToLagoonTasks(); @@ -149,9 +159,40 @@ const messageConsumer = async msg => { return; } + // Create an API token that this task pod can use. It only has permissions + // for the tasks project, and only has access for 1 day. + const apiToken = createJWTWithoutUserId ({ + payload: { + role: 'none', + permissions: { + projects: [project.id], + customers: [], + }, + aud: process.env.JWTAUDIENCE, + iss: 'openshiftjobs', + sub: 'openshiftjobs', + }, + expiresIn: '1d', + jwtSecret: process.env.JWTSECRET, + }); + const cronjobEnvVars = env => env.name === 'CRONJOBS'; const containerEnvLens = R.lensPath(['containers', 0, 'env']); const removeCronjobs = R.over(containerEnvLens, R.reject(cronjobEnvVars)); + const addTaskEnvVars = R.over(containerEnvLens, R.concat([ + { + name: 'TASK_API_HOST', + value: lagoonApiRoute, + }, + { + name: 'TASK_API_AUTH', + value: apiToken, + }, + { + name: 'TASK_DATA_ID', + value: task.id, + }, + ])); const containerCommandLens = R.lensPath(['containers', 0, 'command']); const setContainerCommand = R.set(containerCommandLens, [ @@ -166,6 +207,7 @@ const messageConsumer = async msg => { taskPodSpec = R.pipe( R.prop(task.service), removeCronjobs, + addTaskEnvVars, setContainerCommand, )(oneContainerPerSpec); } catch (err) { From eba8d08d353e964f2b8092ac1edc51ed088013a1 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 20 Dec 2018 21:55:15 -0600 Subject: [PATCH 15/16] Show task files in the UI --- services/ui/src/components/Task/index.js | 22 ++++++++++++++++++++++ services/ui/src/components/Tasks/index.js | 2 -- services/ui/src/pages/tasks.js | 10 ++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/services/ui/src/components/Task/index.js b/services/ui/src/components/Task/index.js index 45c6b27d43..702effc787 100644 --- a/services/ui/src/components/Task/index.js +++ b/services/ui/src/components/Task/index.js @@ -45,6 +45,17 @@ const Task = ({ task }) => (
+ {task.files.length > 0 && +
+
+ + +
+
} diff --git a/services/ui/src/components/Tasks/index.js b/services/ui/src/components/Tasks/index.js index 0e644bdf16..09a0ceeb71 100644 --- a/services/ui/src/components/Tasks/index.js +++ b/services/ui/src/components/Tasks/index.js @@ -43,7 +43,6 @@ const Tasks = ({
-
@@ -71,7 +70,6 @@ const Tasks = ({ .local() .format('DD MMM YYYY, HH:mm:ss')} -
{task.command}
{task.service}
{task.status.charAt(0).toUpperCase() + diff --git a/services/ui/src/pages/tasks.js b/services/ui/src/pages/tasks.js index cc346db6ef..341c9c76df 100644 --- a/services/ui/src/pages/tasks.js +++ b/services/ui/src/pages/tasks.js @@ -38,6 +38,11 @@ const query = gql` command service logs + files { + id + filename + download + } } } } @@ -56,6 +61,11 @@ const subscribe = gql` command service logs + files { + id + filename + download + } } } `; From 5a2ad760b08ef272cf324a534f8a5f0f3a465f6d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 9 Jan 2019 16:47:03 -0600 Subject: [PATCH 16/16] Update "add task" UI to match api --- .../AddTask/components/Completed.js | 3 + .../AddTask/components/DrushArchiveDump.js | 96 ++++++++++++++ .../AddTask/components/DrushRsyncFiles.js | 120 ++++++++++++++++++ .../AddTask/components/DrushSqlSync.js | 120 ++++++++++++++++++ .../components/AddTask/components/Empty.js | 4 + .../components/AddTask/components/Error.js | 3 + .../components/AddTask/components/logic.js | 36 ++++++ services/ui/src/components/AddTask/index.js | 104 +++++++++++++++ services/ui/src/components/AddTask/logic.js | 91 +++++++++++++ services/ui/src/components/Tasks/index.js | 96 +------------- services/ui/src/components/Tasks/logic.js | 28 ---- .../src/components/Tasks/withTaskMutation.js | 44 ------- services/ui/src/pages/tasks.js | 7 +- 13 files changed, 587 insertions(+), 165 deletions(-) create mode 100644 services/ui/src/components/AddTask/components/Completed.js create mode 100644 services/ui/src/components/AddTask/components/DrushArchiveDump.js create mode 100644 services/ui/src/components/AddTask/components/DrushRsyncFiles.js create mode 100644 services/ui/src/components/AddTask/components/DrushSqlSync.js create mode 100644 services/ui/src/components/AddTask/components/Empty.js create mode 100644 services/ui/src/components/AddTask/components/Error.js create mode 100644 services/ui/src/components/AddTask/components/logic.js create mode 100644 services/ui/src/components/AddTask/index.js create mode 100644 services/ui/src/components/AddTask/logic.js delete mode 100644 services/ui/src/components/Tasks/logic.js delete mode 100644 services/ui/src/components/Tasks/withTaskMutation.js diff --git a/services/ui/src/components/AddTask/components/Completed.js b/services/ui/src/components/AddTask/components/Completed.js new file mode 100644 index 0000000000..a2b708d4d0 --- /dev/null +++ b/services/ui/src/components/AddTask/components/Completed.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default () =>
Task added.
; diff --git a/services/ui/src/components/AddTask/components/DrushArchiveDump.js b/services/ui/src/components/AddTask/components/DrushArchiveDump.js new file mode 100644 index 0000000000..f09aa3e109 --- /dev/null +++ b/services/ui/src/components/AddTask/components/DrushArchiveDump.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { Mutation } from 'react-apollo'; +import gql from 'graphql-tag'; +import ReactSelect from 'react-select'; +import { bp, color, fontSize } from '../../../variables'; + +const taskDrushArchiveDump = gql` + mutation taskDrushArchiveDump( + $environment: Int! + ) { + taskDrushArchiveDump( + environment: $environment + ) { + id + name + status + created + started + completed + remoteId + command + service + } + } +`; + +const DrushArchiveDump = ({ + pageEnvironment, + onCompleted, + onError, +}) => ( + + {(taskDrushArchiveDump, { loading, called, error, data }) => { + return ( + +
+ + +
+ + +
+ ); + }} +
+); + +export default DrushArchiveDump; diff --git a/services/ui/src/components/AddTask/components/DrushRsyncFiles.js b/services/ui/src/components/AddTask/components/DrushRsyncFiles.js new file mode 100644 index 0000000000..64255991e9 --- /dev/null +++ b/services/ui/src/components/AddTask/components/DrushRsyncFiles.js @@ -0,0 +1,120 @@ +import React from 'react'; +import { Mutation } from 'react-apollo'; +import gql from 'graphql-tag'; +import ReactSelect from 'react-select'; +import withLogic from './logic'; +import { bp, color, fontSize } from '../../../variables'; + +const taskDrushRsyncFiles = gql` + mutation taskDrushRsyncFiles( + $sourceEnvironment: Int! + $destinationEnvironment: Int! + ) { + taskDrushRsyncFiles( + sourceEnvironment: $sourceEnvironment + destinationEnvironment: $destinationEnvironment + ) { + id + name + status + created + started + completed + remoteId + command + service + } + } +`; + +const DrushRsyncFiles = ({ + pageEnvironment, + projectEnvironments, + selectedSourceEnv, + setSelectedSourceEnv, + onCompleted, + onError, + options, + getEnvName +}) => ( + + {(taskDrushRsyncFiles, { loading, called, error, data }) => { + return ( + +
+ + o.value === selectedSourceEnv)} + onChange={selectedOption => + setSelectedSourceEnv(selectedOption.value) + } + options={options} + required + /> +
+
+ + +
+ + +
+ ); + }} +
+); + +export default withLogic(DrushRsyncFiles); diff --git a/services/ui/src/components/AddTask/components/DrushSqlSync.js b/services/ui/src/components/AddTask/components/DrushSqlSync.js new file mode 100644 index 0000000000..8b923bccb1 --- /dev/null +++ b/services/ui/src/components/AddTask/components/DrushSqlSync.js @@ -0,0 +1,120 @@ +import React from 'react'; +import { Mutation } from 'react-apollo'; +import gql from 'graphql-tag'; +import ReactSelect from 'react-select'; +import withLogic from './logic'; +import { bp, color, fontSize } from '../../../variables'; + +const taskDrushSqlSync = gql` + mutation taskDrushSqlSync( + $sourceEnvironment: Int! + $destinationEnvironment: Int! + ) { + taskDrushSqlSync( + sourceEnvironment: $sourceEnvironment + destinationEnvironment: $destinationEnvironment + ) { + id + name + status + created + started + completed + remoteId + command + service + } + } +`; + +const DrushSqlSync = ({ + pageEnvironment, + projectEnvironments, + selectedSourceEnv, + setSelectedSourceEnv, + onCompleted, + onError, + options, + getEnvName +}) => ( + + {(taskDrushSqlSync, { loading, called, error, data }) => { + return ( + +
+ + o.value === selectedSourceEnv)} + onChange={selectedOption => + setSelectedSourceEnv(selectedOption.value) + } + options={options} + required + /> +
+
+ + +
+ + +
+ ); + }} +
+); + +export default withLogic(DrushSqlSync); diff --git a/services/ui/src/components/AddTask/components/Empty.js b/services/ui/src/components/AddTask/components/Empty.js new file mode 100644 index 0000000000..421db2b9cc --- /dev/null +++ b/services/ui/src/components/AddTask/components/Empty.js @@ -0,0 +1,4 @@ +import React from 'react'; + +export default () => ; + diff --git a/services/ui/src/components/AddTask/components/Error.js b/services/ui/src/components/AddTask/components/Error.js new file mode 100644 index 0000000000..df622bad5c --- /dev/null +++ b/services/ui/src/components/AddTask/components/Error.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default () =>
Error.
; diff --git a/services/ui/src/components/AddTask/components/logic.js b/services/ui/src/components/AddTask/components/logic.js new file mode 100644 index 0000000000..c7c9ee72cc --- /dev/null +++ b/services/ui/src/components/AddTask/components/logic.js @@ -0,0 +1,36 @@ +import React from 'react'; +import withHandlers from 'recompose/withHandlers'; +import withState from 'recompose/withState'; +import withProps from 'recompose/withProps'; +import compose from 'recompose/compose'; + +const withSelectedSourceEnv = withState('selectedSourceEnv', 'setSelectedSourceEnv', ''); +const withEnvironments = withProps(({ projectEnvironments, pageEnvironment }) => { + const allButCurrentEnvironments = projectEnvironments.filter( + env => env.id !== pageEnvironment.id + ); + const options = allButCurrentEnvironments.map(env => ({ + label: env.name, + value: env.id + })); + + return { + options, + }; +}); +const withEnvHandlers = withHandlers({ + getEnvName: ({ projectEnvironments }) => id => { + const environmentObj = projectEnvironments.find(env => env.id === id); + if (!environmentObj) { + return null; + } + + return environmentObj.name; + } +}); + +export default compose( + withSelectedSourceEnv, + withEnvironments, + withEnvHandlers, +); diff --git a/services/ui/src/components/AddTask/index.js b/services/ui/src/components/AddTask/index.js new file mode 100644 index 0000000000..b410bff6e1 --- /dev/null +++ b/services/ui/src/components/AddTask/index.js @@ -0,0 +1,104 @@ +import React from 'react'; +import ReactSelect from 'react-select'; +import withLogic from './logic'; +import DrushArchiveDump from './components/DrushArchiveDump'; +import DrushRsyncFiles from './components/DrushRsyncFiles'; +import DrushSqlSync from './components/DrushSqlSync'; +import Empty from './components/Empty'; +import Completed from './components/Completed'; +import Error from './components/Error'; +import { bp, color, fontSize } from '../../variables'; + +const AddTask = ({ + pageEnvironment, + projectEnvironments, + selectedTask, + setSelectedTask, + onCompleted, + onError, + options +}) => { + const newTaskComponents = { + DrushArchiveDump, + DrushRsyncFiles, + DrushSqlSync, + Empty, + Completed, + Error + }; + + const NewTask = selectedTask + ? newTaskComponents[selectedTask] + : newTaskComponents[Empty]; + + return ( + +
+
+
+ o.value === selectedTask)} + onChange={selectedOption => setSelectedTask(selectedOption.value)} + options={options} + required + /> +
+ {selectedTask && ( +
+ +
+ )} +
+
+ +
+ ); +}; + +export default withLogic(AddTask); diff --git a/services/ui/src/components/AddTask/logic.js b/services/ui/src/components/AddTask/logic.js new file mode 100644 index 0000000000..d7ad7313ab --- /dev/null +++ b/services/ui/src/components/AddTask/logic.js @@ -0,0 +1,91 @@ +import React from 'react'; +import withHandlers from 'recompose/withHandlers'; +import withState from 'recompose/withState'; +import withProps from 'recompose/withProps'; +import compose from 'recompose/compose'; +import gql from 'graphql-tag'; +import { Query } from 'react-apollo'; + +const withSelectedTask = withState('selectedTask', 'setSelectedTask', null); +const withOptions = withProps(({ pageEnvironment }) => { + // Currently all tasks require the environment to have a 'cli' service, + // but this can be made dynamic if that changes. + if (pageEnvironment.services.findIndex(service => service.name === 'cli') === -1) { + return { + options: [], + }; + } + + return { + options: [ + { + label: 'Drush sql-sync', + value: 'DrushSqlSync' + }, + { + label: 'Drush archive-dump', + value: 'DrushArchiveDump' + }, + { + label: 'Drush rsync', + value: 'DrushRsyncFiles' + } + ] + }; +}); +const withNewTaskHanders = withHandlers({ + onCompleted: ({ setSelectedTask }) => () => { + setSelectedTask('Completed'); + }, + onError: ({ setSelectedTask }) => () => { + setSelectedTask('Error'); + } +}); + +const withProjectEnvironments = BaseComponent => + class GetProjectEnvironments extends React.Component { + query = gql` + query getProject($name: String!) { + projectByName(name: $name) { + id + productionEnvironment + environments { + id + name + environmentType + } + } + } + `; + + render() { + const { pageEnvironment } = this.props; + return ( + + {({ loading, error, data }) => { + if (loading || error) { + return null; + } + const allEnvironments = data.projectByName.environments; + + return ( + + ); + }} + + ); + } + }; + +export default compose( + withSelectedTask, + withNewTaskHanders, + withOptions, + withProjectEnvironments +); diff --git a/services/ui/src/components/Tasks/index.js b/services/ui/src/components/Tasks/index.js index 09a0ceeb71..68039c328c 100644 --- a/services/ui/src/components/Tasks/index.js +++ b/services/ui/src/components/Tasks/index.js @@ -1,45 +1,16 @@ import React from 'react'; import Link from 'next/link'; -import compose from 'recompose/compose'; import moment from 'moment'; import momentDurationFormatSetup from 'moment-duration-format'; -import ReactSelect from 'react-select'; -import withTaskMutation from './withTaskMutation'; -import withLogic from './logic'; +import AddTask from '../AddTask'; import { bp, color, fontSize } from '../../variables'; const Tasks = ({ - environmentId, - projectName, + pageEnvironment, tasks, - onSubmit, - formValues, - setFormValues, }) => (
-
-
-
- { - setFormValues(e); - }} - getOptionLabel={option => option.name} - getOptionValue={option => option.command} - options={[ - { name: 'Site Status', command: 'drush status', service: 'cli' }, - { name: 'Drupal Archive', command: 'drush archive-dump', service: 'cli' } - ]} - required - /> -
- -
-
+
@@ -57,7 +28,7 @@ const Tasks = ({ href={{ pathname: '/tasks', query: { - name: projectName, + name: pageEnvironment.openshiftProjectName, task_id: task.id, } }} @@ -84,60 +55,6 @@ const Tasks = ({ .content { padding: 32px calc((100vw / 16) * 1); width: 100%; - .taskFormWrapper { - @media ${bp.wideUp} { - display: flex; - } - &::before { - @media ${bp.wideUp} { - content: ''; - display: block; - flex-grow: 1; - } - } - } - .taskForm { - background: ${color.white}; - border: 1px solid ${color.lightestGrey}; - border-radius: 3px; - box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.03); - display: flex; - flex-flow: column; - margin-bottom: 32px; - padding: 32px 20px; - @media ${bp.tabletUp} { - margin-bottom: 0; - } - @media ${bp.tinyUp} { - flex-flow: row; - justify-content: flex-end; - } - @media ${bp.wideUp} { - min-width: 52%; - } - .selectTask { - flex-grow: 1; - margin: 0 0 20px; - min-width: 220px; - @media ${bp.tinyUp} { - margin: 0 20px 0 0; - } - } - button { - align-self: flex-end; - background-color: ${color.lightestGrey}; - border: none; - border-radius: 20px; - color: ${color.darkGrey}; - font-family: 'source-code-pro', sans-serif; - ${fontSize(13)}; - padding: 3px 20px 2px; - text-transform: uppercase; - @media ${bp.tinyUp} { - align-self: auto; - } - } - } .header { @media ${bp.tinyUp} { align-items: center; @@ -247,7 +164,4 @@ const Tasks = ({
); -export default compose( - withTaskMutation, - withLogic, -)(Tasks); +export default Tasks; diff --git a/services/ui/src/components/Tasks/logic.js b/services/ui/src/components/Tasks/logic.js deleted file mode 100644 index 4fd2f11fa4..0000000000 --- a/services/ui/src/components/Tasks/logic.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import withHandlers from 'recompose/withHandlers'; -import withState from 'recompose/withState'; -import compose from 'recompose/compose'; -import moment from 'moment'; - -const withFormState = withState('formValues', 'setFormValues', {}); -const withSubmitForm = withHandlers({ - onSubmit: ({ - environmentId, - formValues, - addTask, - }) => e => { - addTask( - formValues.name, - environmentId, - formValues.service, - formValues.command, - moment.utc().format('YYYY-MM-DD HH:mm:ss'), - 'ACTIVE', - ); - }, -}); - -export default compose( - withFormState, - withSubmitForm, -); diff --git a/services/ui/src/components/Tasks/withTaskMutation.js b/services/ui/src/components/Tasks/withTaskMutation.js deleted file mode 100644 index 24dc061325..0000000000 --- a/services/ui/src/components/Tasks/withTaskMutation.js +++ /dev/null @@ -1,44 +0,0 @@ -import gql from 'graphql-tag'; -import { graphql } from 'react-apollo'; - -const addTask = gql` - mutation addTask($input: TaskInput!) { - addTask(input: $input) { - id - name - status - created - started - completed - remoteId - command - service - logs - } - } -`; - -export default graphql(addTask, { - props: ({ mutate }) => ({ - addTask: ( - name, - environment, - service, - command, - created, - status, - ) => - mutate({ - variables: { - input: { - name, - environment, - service, - command, - created, - status, - }, - }, - }), - }), -}); diff --git a/services/ui/src/pages/tasks.js b/services/ui/src/pages/tasks.js index 341c9c76df..a220d93180 100644 --- a/services/ui/src/pages/tasks.js +++ b/services/ui/src/pages/tasks.js @@ -27,6 +27,10 @@ const query = gql` project { name } + services { + id + name + } tasks { id name @@ -157,8 +161,7 @@ const PageTasks = withRouter(props => { /> {!props.router.query.task_id && ( )}