From a38128c74daf5b339d00122a659f834a194177b9 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Tue, 4 Jul 2023 15:23:23 +1000 Subject: [PATCH 1/4] feat: support to cancel tasks --- node-packages/commons/src/tasks.ts | 5 + services/api/src/mocks.js | 1 + services/api/src/resolvers.js | 2 + services/api/src/resources/task/helpers.ts | 442 +++++++++++------- services/api/src/resources/task/resolvers.ts | 80 +++- services/api/src/resources/task/sql.ts | 5 + .../task/task_definition_resolvers.ts | 4 +- services/api/src/typeDefs.js | 11 + .../startup-scripts/00-configure-lagoon.sh | 53 +++ 9 files changed, 417 insertions(+), 186 deletions(-) diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index 2ca470b440..1a59073ece 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -1302,6 +1302,11 @@ export const createMiscTask = async function(taskData: any) { } miscTaskData.advancedTask = taskData.data.advancedTask break; + case 'kubernetes:task:cancel': + // task cancellation is just a standard unmodified message + miscTaskData.misc = taskData.data.build + miscTaskData.misc.command = "" // the command isn't required and just bloats the message + break; case 'kubernetes:build:cancel': // build cancellation is just a standard unmodified message miscTaskData.misc = taskData.data.build diff --git a/services/api/src/mocks.js b/services/api/src/mocks.js index a4c1f98028..34ae91f6c5 100644 --- a/services/api/src/mocks.js +++ b/services/api/src/mocks.js @@ -706,6 +706,7 @@ mocks.Mutation = () => ({ taskDrushRsyncFiles: () => mocks.Task(), deleteTask: () => 'success', updateTask: () => mocks.Task(), + cancelTask: () => faker.random.arrayElement(['success', 'Task not cancelled, reason: Too slow.']), setEnvironmentServices: () => [ mocks.EnvironmentService() ], uploadFilesForTask: () => mocks.Task(), deleteFilesForTask: () => 'success', diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 756c236762..36f8c232fe 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -68,6 +68,7 @@ const { addTask, deleteTask, updateTask, + cancelTask, taskDrushArchiveDump, taskDrushSqlDump, taskDrushCacheClear, @@ -599,6 +600,7 @@ const resolvers = { taskDrushUserLogin, deleteTask, updateTask, + cancelTask, setEnvironmentServices, uploadFilesForTask, deleteFilesForTask, diff --git a/services/api/src/resources/task/helpers.ts b/services/api/src/resources/task/helpers.ts index 317e34edbe..c364a8fbbc 100644 --- a/services/api/src/resources/task/helpers.ts +++ b/services/api/src/resources/task/helpers.ts @@ -1,113 +1,39 @@ import * as R from 'ramda'; import { Pool } from 'mariadb'; +import { asyncPipe } from '@lagoon/commons/dist/util/func'; import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger'; import { createTaskTask, createMiscTask } from '@lagoon/commons/dist/tasks'; -import { query } from '../../util/db'; +import { knex, query } from '../../util/db'; import { pubSub, EVENTS } from '../../clients/pubSub'; import { Sql } from './sql'; import { Sql as projectSql } from '../project/sql'; import { Sql as environmentSql } from '../environment/sql'; -import convertDateToMYSQLDateTimeFormat from '../../util/convertDateToMYSQLDateTimeFormat'; - -export const Helpers = (sqlClientPool: Pool) => ({ - addTask: async ({ - id, - name, - taskName, - status, - created, - started, - completed, - environment, - service, - command, - remoteId, - deployTokenInjection, - projectKeyInjection, - adminOnlyView, - execute - }: { - id?: number; - name: string; - taskName: string; - status?: string; - created?: string; - started?: string; - completed?: string; - environment: number; - service: string; - command: string; - remoteId?: string; - deployTokenInjection: boolean; - projectKeyInjection: boolean; - adminOnlyView: boolean; - execute: boolean; - }) => { - const { insertId } = await query( - sqlClientPool, - Sql.insertTask({ - id, - name, - taskName, - status, - created, - started, - completed, - environment, - service, - command, - deployTokenInjection, - projectKeyInjection, - adminOnlyView, - remoteId, - }), - ); +import { Helpers as environmentHelpers } from '../environment/helpers'; +// import convertDateToMYSQLDateTimeFormat from '../../util/convertDateToMYSQLDateTimeFormat'; - let rows = await query(sqlClientPool, Sql.selectTask(insertId)); - const taskData = R.prop(0, rows); +export const Helpers = (sqlClientPool: Pool, hasPermission) => { + const getTaskById = async (TaskID: number) => { + const queryString = knex('task') + .where('id', '=', TaskID) + .toString(); - pubSub.publish(EVENTS.TASK, taskData); + const rows = await query(sqlClientPool, queryString); + const task = R.prop(0, rows); - // Allow creating task data w/o executing the task - if (execute === false) { - return taskData; + if (!task) { + return null; } - rows = await query( - sqlClientPool, - environmentSql.selectEnvironmentById(taskData.environment) - ); - const environmentData = R.prop(0, rows); - - rows = await query( - sqlClientPool, - projectSql.selectProject(environmentData.project) - ); - const projectData = R.prop(0, rows); - - taskData.id = taskData.id.toString(); + const rowsPerms = await query(sqlClientPool, Sql.selectPermsForTask(task.id)); + await hasPermission('task', 'view', { + project: R.path(['0', 'pid'], rowsPerms) + }); - 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 task; + }; - return taskData; - }, - addAdvancedTask: async ( - { + return { + addTask: async ({ id, name, taskName, @@ -117,48 +43,94 @@ export const Helpers = (sqlClientPool: Pool) => ({ completed, environment, service, - image, - payload = {}, + command, remoteId, - execute, deployTokenInjection, projectKeyInjection, adminOnlyView, + execute }: { - id?: number, - name: string, - taskName: string, - status?: string, - created?: string, - started?: string, - completed?: string, - environment: number, - service: string, - image: string, - payload: object, - remoteId?: string, - execute: boolean, - deployTokenInjection: boolean, - projectKeyInjection: boolean, - adminOnlyView: boolean, + id?: number; + name: string; + taskName: string; + status?: string; + created?: string; + started?: string; + completed?: string; + environment: number; + service: string; + command: string; + remoteId?: string; + deployTokenInjection: boolean; + projectKeyInjection: boolean; + adminOnlyView: boolean; + execute: boolean; + }) => { + const { insertId } = await query( + sqlClientPool, + Sql.insertTask({ + id, + name, + taskName, + status, + created, + started, + completed, + environment, + service, + command, + deployTokenInjection, + projectKeyInjection, + adminOnlyView, + remoteId, + }), + ); + + let rows = await query(sqlClientPool, Sql.selectTask(insertId)); + const taskData = R.prop(0, rows); + + pubSub.publish(EVENTS.TASK, taskData); + + // Allow creating task data w/o executing the task + if (execute === false) { + return taskData; + } + + rows = await query( + sqlClientPool, + environmentSql.selectEnvironmentById(taskData.environment) + ); + const environmentData = R.prop(0, rows); + + rows = await query( + sqlClientPool, + projectSql.selectProject(environmentData.project) + ); + const projectData = R.prop(0, rows); + + taskData.id = taskData.id.toString(); + + 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; }, - ) => { - let rows = await query( - sqlClientPool, - environmentSql.selectEnvironmentById(environment), - ); - const environmentData = R.prop(0, rows); - - rows = await query( - sqlClientPool, - projectSql.selectProject(environmentData.project), - ); - const projectData = R.prop(0, rows); - - - const queryresp = await query( - sqlClientPool, - Sql.insertTask({ + addAdvancedTask: async ( + { id, name, taskName, @@ -168,54 +140,174 @@ export const Helpers = (sqlClientPool: Pool) => ({ completed, environment, service, - command: image, + image, + payload = {}, + remoteId, + execute, deployTokenInjection, projectKeyInjection, adminOnlyView, - remoteId, - type: 'advanced', - advanced_image: image, - advanced_payload: JSON.stringify(payload), - }), - ); - - const { insertId } = queryresp; - rows = await query(sqlClientPool, Sql.selectTask(insertId)); - const taskData = R.prop(0, rows); - const ADVANCED_TASK_EVENT_TYPE = "task:advanced" - - taskData.id = taskData.id.toString(); - - let jobSpec = { - task: taskData, - project: projectData, - environment: environmentData, - advancedTask: { - RunnerImage: image, - JSONPayload: new Buffer(JSON.stringify(payload).replace(/\\n/g, "\n")).toString('base64'), - deployerToken: deployTokenInjection, //an admintask will have a deployer token and ssh key injected into it - sshKey: projectKeyInjection, - } - } + }: { + id?: number, + name: string, + taskName: string, + status?: string, + created?: string, + started?: string, + completed?: string, + environment: number, + service: string, + image: string, + payload: object, + remoteId?: string, + execute: boolean, + deployTokenInjection: boolean, + projectKeyInjection: boolean, + adminOnlyView: boolean, + }, + ) => { + let rows = await query( + sqlClientPool, + environmentSql.selectEnvironmentById(environment), + ); + const environmentData = R.prop(0, rows); + + rows = await query( + sqlClientPool, + projectSql.selectProject(environmentData.project), + ); + const projectData = R.prop(0, rows); + + + const queryresp = await query( + sqlClientPool, + Sql.insertTask({ + id, + name, + taskName, + status, + created, + started, + completed, + environment, + service, + command: image, + deployTokenInjection, + projectKeyInjection, + adminOnlyView, + remoteId, + type: 'advanced', + advanced_image: image, + advanced_payload: JSON.stringify(payload), + }), + ); - try { - await createMiscTask( - { - key: ADVANCED_TASK_EVENT_TYPE, - data: jobSpec + const { insertId } = queryresp; + rows = await query(sqlClientPool, Sql.selectTask(insertId)); + const taskData = R.prop(0, rows); + const ADVANCED_TASK_EVENT_TYPE = "task:advanced" + + taskData.id = taskData.id.toString(); + + let jobSpec = { + task: taskData, + project: projectData, + environment: environmentData, + advancedTask: { + RunnerImage: image, + JSONPayload: new Buffer(JSON.stringify(payload).replace(/\\n/g, "\n")).toString('base64'), + deployerToken: deployTokenInjection, //an admintask will have a deployer token and ssh key injected into it + sshKey: projectKeyInjection, } - ) - } catch (error) { - sendToLagoonLogs( - 'error', - projectData.name, - '', - 'api:addTask', - { taskId: taskData.id }, - `*[${projectData.name}]* Task not initiated, reason: ${error}`, + } + + try { + await createMiscTask( + { + key: ADVANCED_TASK_EVENT_TYPE, + data: jobSpec + } + ) + } catch (error) { + sendToLagoonLogs( + 'error', + projectData.name, + '', + 'api:addTask', + { taskId: taskData.id }, + `*[${projectData.name}]* Task not initiated, reason: ${error}`, + ); + } + + return taskData; + }, + getTaskByTaskInput: async taskInput => { + const notEmpty = R.complement(R.anyPass([R.isNil, R.isEmpty])); + const hasId = R.both(R.has('id'), R.propSatisfies(notEmpty, 'id')); + const hasName = R.both(R.has('taskName'), R.propSatisfies(notEmpty, 'taskName')); + const hasEnvironment = R.both( + R.has('environment'), + R.propSatisfies(notEmpty, 'environment') ); - } + // @ts-ignore + const hasNameAndEnvironment = R.both(hasName, hasEnvironment); + + const taskFromId = asyncPipe( + R.prop('id'), + getTaskById, + task => { + if (!task) { + throw new Error('Unauthorized'); + } - return taskData; - }, -}); + return task; + } + ); + + const taskFromNameEnv = async input => { + const environments = await environmentHelpers( + sqlClientPool + ).getEnvironmentsByEnvironmentInput(R.prop('environment', input)); + const activeEnvironments = R.filter( + R.propEq('deleted', '0000-00-00 00:00:00'), + environments + ); + + if (activeEnvironments.length < 1 || activeEnvironments.length > 1) { + throw new Error('Unauthorized'); + } + + const environment = R.prop(0, activeEnvironments); + + const rows = await query( + sqlClientPool, + Sql.selectTaskByNameAndEnvironment( + R.prop('taskName', input), + environment.id + ) + ); + + if (!R.prop(0, rows)) { + throw new Error('Unauthorized'); + } + + return R.prop(0, rows); + }; + + return R.cond([ + [hasId, taskFromId], + // @ts-ignore + [hasNameAndEnvironment, taskFromNameEnv], + [ + R.T, + () => { + throw new Error( + 'Must provide task (id) or (name and environment)' + ); + } + ] + // @ts-ignore + ])(taskInput); + } + }; +}; diff --git a/services/api/src/resources/task/resolvers.ts b/services/api/src/resources/task/resolvers.ts index 3b9aa614cf..48dcc4e900 100644 --- a/services/api/src/resources/task/resolvers.ts +++ b/services/api/src/resources/task/resolvers.ts @@ -15,7 +15,9 @@ import { Validators as envValidators } from '../environment/validators'; import S3 from 'aws-sdk/clients/s3'; import sha1 from 'sha1'; import { generateTaskName } from '@lagoon/commons/dist/util/lagoon'; -import { logger } from '../../loggers/logger'; +// import { logger } from '../../loggers/logger'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger'; +import { createMiscTask } from '@lagoon/commons/dist/tasks'; const accessKeyId = process.env.S3_FILES_ACCESS_KEY_ID || 'minio' const secretAccessKey = process.env.S3_FILES_SECRET_ACCESS_KEY || 'minio123' @@ -283,7 +285,7 @@ export const addTask: ResolverFn = async ( } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ id, name, taskName, @@ -329,6 +331,66 @@ export const deleteTask: ResolverFn = async ( return 'success'; }; +export const cancelTask: ResolverFn = async ( + root, + { + input: { + task: taskInput, + } + }, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + + const task = await Helpers(sqlClientPool, hasPermission).getTaskByTaskInput(taskInput); + const environment = await environmentHelpers(sqlClientPool).getEnvironmentById(task.environment); + const project = await projectHelpers(sqlClientPool).getProjectById(environment.project); + + if (!task) { + return null; + } + + const envPerm = await environmentHelpers(sqlClientPool).getEnvironmentById( + task.environment + ); + await hasPermission('task', `cancel:${envPerm.environmentType}`, { + project: project.id + }); + + + const data = { + build: task, + environment, + project + }; + + userActivityLogger( + `User cancelled task for '${task.environment}'`, + { + project: '', + event: 'api:cancelDeployment', + payload: { + taskInput, + data: data.build + } + } + ); + + try { + await createMiscTask({ key: 'task:cancel', data }); + return 'success'; + } catch (error) { + sendToLagoonLogs( + 'error', + '', + '', + 'api:cancelTask', + { taskId: task.id }, + `Task not cancelled, reason: ${error}` + ); + return `Error: ${error.message}`; + } +}; + export const updateTask: ResolverFn = async ( root, { @@ -450,7 +512,7 @@ TOKEN="$(ssh -p `+"${LAGOON_CONFIG_TOKEN_PORT:-$TASK_SSH_PORT}"+` -t lagoon@`+"$ } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: 'Drush archive-dump', taskName: generateTaskName(), environment: environmentId, @@ -499,7 +561,7 @@ TOKEN="$(ssh -p `+"${LAGOON_CONFIG_TOKEN_PORT:-$TASK_SSH_PORT}"+` -t lagoon@`+"$ } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: 'Drush sql-dump', taskName: generateTaskName(), environment: environmentId, @@ -550,7 +612,7 @@ export const taskDrushCacheClear: ResolverFn = async ( } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: 'Drush cache-clear', taskName: generateTaskName(), environment: environmentId, @@ -590,7 +652,7 @@ export const taskDrushCron: ResolverFn = async ( } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: 'Drush cron', taskName: generateTaskName(), environment: environmentId, @@ -662,7 +724,7 @@ export const taskDrushSqlSync: ResolverFn = async ( if [[ ! "" = "$(drush | grep 'lagoon:aliases')" ]]; then LAGOON_ALIAS_PREFIX="lagoon.\${LAGOON_PROJECT}-"; fi && \ drush -y sql-sync @\${LAGOON_ALIAS_PREFIX}${sourceEnvironment.name} @self`; - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: `Sync DB ${sourceEnvironment.name} -> ${destinationEnvironment.name}`, taskName: generateTaskName(), environment: destinationEnvironmentId, @@ -734,7 +796,7 @@ export const taskDrushRsyncFiles: ResolverFn = async ( if [[ ! "" = "$(drush | grep 'lagoon:aliases')" ]]; then LAGOON_ALIAS_PREFIX="lagoon.\${LAGOON_PROJECT}-"; fi && \ drush -y rsync @\${LAGOON_ALIAS_PREFIX}${sourceEnvironment.name}:%files @self:%files -- --omit-dir-times --no-perms --no-group --no-owner --chmod=ugo=rwX`; - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: `Sync files ${sourceEnvironment.name} -> ${destinationEnvironment.name}`, taskName: generateTaskName(), environment: destinationEnvironmentId, @@ -774,7 +836,7 @@ export const taskDrushUserLogin: ResolverFn = async ( } }); - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: 'Drush uli', taskName: generateTaskName(), environment: environmentId, diff --git a/services/api/src/resources/task/sql.ts b/services/api/src/resources/task/sql.ts index 329921565f..ad93d7953a 100644 --- a/services/api/src/resources/task/sql.ts +++ b/services/api/src/resources/task/sql.ts @@ -5,6 +5,11 @@ export const Sql = { knex('task') .where('task.id', '=', id) .toString(), + selectTaskByNameAndEnvironment: (name: string, environmentId: number) => + knex('task') + .where('task_name', '=', name) + .andWhere('environment', '=', environmentId) + .toString(), insertTask: ({ id, name, diff --git a/services/api/src/resources/task/task_definition_resolvers.ts b/services/api/src/resources/task/task_definition_resolvers.ts index e40c0e0bcf..2c4b6c67a8 100644 --- a/services/api/src/resources/task/task_definition_resolvers.ts +++ b/services/api/src/resources/task/task_definition_resolvers.ts @@ -577,7 +577,7 @@ export const invokeRegisteredTask = async ( taskCommand += `${task.command}`; - const taskData = await Helpers(sqlClientPool).addTask({ + const taskData = await Helpers(sqlClientPool, hasPermission).addTask({ name: task.name, taskName: generateTaskName(), environment: environment, @@ -603,7 +603,7 @@ export const invokeRegisteredTask = async ( } - const advancedTaskData = await Helpers(sqlClientPool).addAdvancedTask({ + const advancedTaskData = await Helpers(sqlClientPool, hasPermission).addAdvancedTask({ name: task.name, taskName: generateTaskName(), created: undefined, diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index d1ecd11f2b..4eb2f421c2 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -1525,6 +1525,16 @@ const typeDefs = gql` patch: UpdateTaskPatchInput! } + input CancelTaskNameInput { + id: Int + taskName: String + environment: EnvironmentInput + } + + input CancelTaskInput { + task: CancelTaskNameInput! + } + input AddOpenshiftInput { id: Int name: String! @@ -2125,6 +2135,7 @@ const typeDefs = gql` taskDrushUserLogin(environment: Int!): Task deleteTask(input: DeleteTaskInput!): String updateTask(input: UpdateTaskInput): Task + cancelTask(input: CancelTaskInput!): String setEnvironmentServices(input: SetEnvironmentServicesInput!): [EnvironmentService] uploadFilesForTask(input: UploadFilesForTaskInput!): Task deleteFilesForTask(input: DeleteFilesForTaskInput!): String diff --git a/services/keycloak/startup-scripts/00-configure-lagoon.sh b/services/keycloak/startup-scripts/00-configure-lagoon.sh index e0b640a72d..fdfbbc83be 100755 --- a/services/keycloak/startup-scripts/00-configure-lagoon.sh +++ b/services/keycloak/startup-scripts/00-configure-lagoon.sh @@ -2275,6 +2275,57 @@ function change_project_groupadd_to_owner_role { EOF } +function add_development_task_cancel { + CLIENT_ID=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients?clientId=api --config $CONFIG_PATH | jq -r '.[0]["id"]') + cancel_development_task=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/permission?name=Cancel+Development+Task --config $CONFIG_PATH) + + if [ "$cancel_development_task" != "[ ]" ]; then + echo "task:cancel:development already configured" + return 0 + fi + + echo Configuring task:cancel:development + + TASK_RESOURCE_ID=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/resource?name=task --config $CONFIG_PATH | jq -r '.[0]["_id"]') + /opt/jboss/keycloak/bin/kcadm.sh update clients/$CLIENT_ID/authz/resource-server/resource/$TASK_RESOURCE_ID --config $CONFIG_PATH -r ${KEYCLOAK_REALM:-master} -s 'scopes=[{"name":"view"},{"name":"update"},{"name":"delete"},{"name":"add:production"},{"name":"add:development"},{"name":"addNoExec"},{"name":"drushArchiveDump:development"},{"name":"drushArchiveDump:production"},{"name":"drushSqlDump:development"},{"name":"drushSqlDump:production"},{"name":"drushCacheClear:development"},{"name":"drushCacheClear:production"},{"name":"drushCron:development"},{"name":"drushCron:production"},{"name":"drushUserLogin:development"},{"name":"drushUserLogin:production"},{"name":"drushSqlSync:source:development"},{"name":"drushSqlSync:source:production"},{"name":"drushSqlSync:destination:development"},{"name":"drushSqlSync:destination:production"},{"name":"drushRsync:source:development"},{"name":"drushRsync:source:production"},{"name":"drushRsync:destination:development"},{"name":"drushRsync:destination:production"},{"name":"cancel:development"},{"name":"cancel:production"}]' + + /opt/jboss/keycloak/bin/kcadm.sh create clients/$CLIENT_ID/authz/resource-server/permission/scope --config $CONFIG_PATH -r lagoon -f - < Date: Tue, 4 Jul 2023 21:24:17 +1000 Subject: [PATCH 2/4] fix: only update the task resource --- services/keycloak/startup-scripts/00-configure-lagoon.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/keycloak/startup-scripts/00-configure-lagoon.sh b/services/keycloak/startup-scripts/00-configure-lagoon.sh index fdfbbc83be..88a0aba1c1 100755 --- a/services/keycloak/startup-scripts/00-configure-lagoon.sh +++ b/services/keycloak/startup-scripts/00-configure-lagoon.sh @@ -2286,7 +2286,8 @@ function add_development_task_cancel { echo Configuring task:cancel:development - TASK_RESOURCE_ID=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/resource?name=task --config $CONFIG_PATH | jq -r '.[0]["_id"]') + # select the one for tasks, because 'name=task' is a search term and returns advanced_task and task in the response, we only want to modify the 'task' resource + TASK_RESOURCE_ID=$(/opt/jboss/keycloak/bin/kcadm.sh get -r lagoon clients/$CLIENT_ID/authz/resource-server/resource?name=task --config $CONFIG_PATH | jq -r '.[]| select(.name=="task")|."_id"') /opt/jboss/keycloak/bin/kcadm.sh update clients/$CLIENT_ID/authz/resource-server/resource/$TASK_RESOURCE_ID --config $CONFIG_PATH -r ${KEYCLOAK_REALM:-master} -s 'scopes=[{"name":"view"},{"name":"update"},{"name":"delete"},{"name":"add:production"},{"name":"add:development"},{"name":"addNoExec"},{"name":"drushArchiveDump:development"},{"name":"drushArchiveDump:production"},{"name":"drushSqlDump:development"},{"name":"drushSqlDump:production"},{"name":"drushCacheClear:development"},{"name":"drushCacheClear:production"},{"name":"drushCron:development"},{"name":"drushCron:production"},{"name":"drushUserLogin:development"},{"name":"drushUserLogin:production"},{"name":"drushSqlSync:source:development"},{"name":"drushSqlSync:source:production"},{"name":"drushSqlSync:destination:development"},{"name":"drushSqlSync:destination:production"},{"name":"drushRsync:source:development"},{"name":"drushRsync:source:production"},{"name":"drushRsync:destination:development"},{"name":"drushRsync:destination:production"},{"name":"cancel:development"},{"name":"cancel:production"}]' /opt/jboss/keycloak/bin/kcadm.sh create clients/$CLIENT_ID/authz/resource-server/permission/scope --config $CONFIG_PATH -r lagoon -f - < Date: Wed, 5 Jul 2023 10:28:45 +1000 Subject: [PATCH 3/4] refactor: make sure the payload is what the remote-controller is expecting --- node-packages/commons/src/tasks.ts | 3 +-- .../handler/controller_tasks.go | 22 ++++++++++++++----- services/api/src/resources/task/resolvers.ts | 9 ++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index 1a59073ece..b790ba7835 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -1304,8 +1304,7 @@ export const createMiscTask = async function(taskData: any) { break; case 'kubernetes:task:cancel': // task cancellation is just a standard unmodified message - miscTaskData.misc = taskData.data.build - miscTaskData.misc.command = "" // the command isn't required and just bloats the message + miscTaskData.misc = taskData.data.task break; case 'kubernetes:build:cancel': // build cancellation is just a standard unmodified message diff --git a/services/actions-handler/handler/controller_tasks.go b/services/actions-handler/handler/controller_tasks.go index a054032939..20b01cf2b7 100644 --- a/services/actions-handler/handler/controller_tasks.go +++ b/services/actions-handler/handler/controller_tasks.go @@ -18,7 +18,13 @@ import ( ) func (m *Messenger) handleTask(ctx context.Context, messageQueue mq.MQ, message *schema.LagoonMessage, messageID string) { - prefix := fmt.Sprintf("(messageid:%s) %s/%s: ", messageID, message.Namespace, message.Meta.Task.Name) + + // use the preferred TaskName value + prefix := fmt.Sprintf("(messageid:%s) %s/%s: ", messageID, message.Namespace, message.Meta.Task.TaskName) + if message.Meta.Task.TaskName == "" { + // or fall back to the older task name (the full name could be "my custom task" which isn't great) + prefix = fmt.Sprintf("(messageid:%s) %s/%s: ", messageID, message.Namespace, message.Meta.Task.Name) + } log.Println(fmt.Sprintf("%sreceived task status update: %s", prefix, message.Meta.JobStatus)) // generate a lagoon token with a expiry of 60 seconds from now token, err := jwt.GenerateAdminToken(m.LagoonAPI.TokenSigningKey, m.LagoonAPI.JWTAudience, m.LagoonAPI.JWTSubject, m.LagoonAPI.JWTIssuer, time.Now().Unix(), 60) @@ -86,10 +92,16 @@ func (m *Messenger) handleTask(ctx context.Context, messageQueue mq.MQ, message taskId, _ := strconv.Atoi(message.Meta.Task.ID) // prepare the task patch for later step updateTaskPatch := schema.UpdateTaskPatchInput{ - RemoteID: message.Meta.RemoteID, - Status: schema.StatusTypes(strings.ToUpper(message.Meta.JobStatus)), - Started: message.Meta.StartTime, - Completed: message.Meta.EndTime, + Status: schema.StatusTypes(strings.ToUpper(message.Meta.JobStatus)), + } + if message.Meta.RemoteID != "" { + updateTaskPatch.RemoteID = message.Meta.RemoteID + } + if message.Meta.StartTime != "" { + updateTaskPatch.Started = message.Meta.StartTime + } + if message.Meta.EndTime != "" { + updateTaskPatch.Completed = message.Meta.EndTime } updatedTask, err := lagoon.UpdateTask(ctx, taskId, updateTaskPatch, l) if err != nil { diff --git a/services/api/src/resources/task/resolvers.ts b/services/api/src/resources/task/resolvers.ts index 48dcc4e900..0c622a7653 100644 --- a/services/api/src/resources/task/resolvers.ts +++ b/services/api/src/resources/task/resolvers.ts @@ -358,7 +358,12 @@ export const cancelTask: ResolverFn = async ( const data = { - build: task, + task: { + id: task.id.toString(), + name: task.name, + taskName: task.taskName, + service: task.service, + }, environment, project }; @@ -370,7 +375,7 @@ export const cancelTask: ResolverFn = async ( event: 'api:cancelDeployment', payload: { taskInput, - data: data.build + data: data.task } } ); From 10380560c1f82dd359fdf086fc33a2cd6826f124 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Mon, 17 Jul 2023 11:51:46 +1000 Subject: [PATCH 4/4] fix: change error results and other minor fixes --- services/api/src/resources/task/helpers.ts | 9 ++++----- services/api/src/resources/task/resolvers.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/services/api/src/resources/task/helpers.ts b/services/api/src/resources/task/helpers.ts index c364a8fbbc..baa0e317d6 100644 --- a/services/api/src/resources/task/helpers.ts +++ b/services/api/src/resources/task/helpers.ts @@ -9,7 +9,6 @@ import { Sql } from './sql'; import { Sql as projectSql } from '../project/sql'; import { Sql as environmentSql } from '../environment/sql'; import { Helpers as environmentHelpers } from '../environment/helpers'; -// import convertDateToMYSQLDateTimeFormat from '../../util/convertDateToMYSQLDateTimeFormat'; export const Helpers = (sqlClientPool: Pool, hasPermission) => { const getTaskById = async (TaskID: number) => { @@ -257,7 +256,7 @@ export const Helpers = (sqlClientPool: Pool, hasPermission) => { getTaskById, task => { if (!task) { - throw new Error('Unauthorized'); + throw new Error('No matching task found'); } return task; @@ -273,8 +272,8 @@ export const Helpers = (sqlClientPool: Pool, hasPermission) => { environments ); - if (activeEnvironments.length < 1 || activeEnvironments.length > 1) { - throw new Error('Unauthorized'); + if (activeEnvironments.length != 1) { + throw new Error('No matching environment found'); } const environment = R.prop(0, activeEnvironments); @@ -288,7 +287,7 @@ export const Helpers = (sqlClientPool: Pool, hasPermission) => { ); if (!R.prop(0, rows)) { - throw new Error('Unauthorized'); + throw new Error('No matching task found'); } return R.prop(0, rows); diff --git a/services/api/src/resources/task/resolvers.ts b/services/api/src/resources/task/resolvers.ts index 0c622a7653..89dd4e8e53 100644 --- a/services/api/src/resources/task/resolvers.ts +++ b/services/api/src/resources/task/resolvers.ts @@ -15,7 +15,6 @@ import { Validators as envValidators } from '../environment/validators'; import S3 from 'aws-sdk/clients/s3'; import sha1 from 'sha1'; import { generateTaskName } from '@lagoon/commons/dist/util/lagoon'; -// import { logger } from '../../loggers/logger'; import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger'; import { createMiscTask } from '@lagoon/commons/dist/tasks'; @@ -342,17 +341,14 @@ export const cancelTask: ResolverFn = async ( ) => { const task = await Helpers(sqlClientPool, hasPermission).getTaskByTaskInput(taskInput); - const environment = await environmentHelpers(sqlClientPool).getEnvironmentById(task.environment); - const project = await projectHelpers(sqlClientPool).getProjectById(environment.project); - if (!task) { return null; } - const envPerm = await environmentHelpers(sqlClientPool).getEnvironmentById( - task.environment - ); - await hasPermission('task', `cancel:${envPerm.environmentType}`, { + const environment = await environmentHelpers(sqlClientPool).getEnvironmentById(task.environment); + const project = await projectHelpers(sqlClientPool).getProjectById(environment.project); + + await hasPermission('task', `cancel:${environment.environmentType}`, { project: project.id });