Skip to content

Commit

Permalink
feat: allow registration of systemwide tasks (#3618)
Browse files Browse the repository at this point in the history
  • Loading branch information
bomoko authored Feb 6, 2024
1 parent da37495 commit 9f2e8ae
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 20 deletions.
4 changes: 4 additions & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Deprecations will be tracked by the release they are announced in, and then upda

All deprecations are listed below, with the most recent announcements at the top.

### Lagoon v2.18.0
release link: https://github.com/uselagoon/lagoon/releases/tag/v2.18.0
* The standard drupal based tasks that Lagoon ships with (drush ....) have been flagged as deprecated and should not be used anymore. These will need to be replaced with [custom tasks](https://docs.lagoon.sh/using-lagoon-advanced/custom-tasks/). Example replacement tasks will be provided prior to their removal.

### Lagoon v2.17.0

release link: https://github.com/uselagoon/lagoon/releases/tag/v2.17.0
Expand Down
12 changes: 12 additions & 0 deletions docs/using-lagoon-advanced/custom-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ The values that the user selects will be available as environment variables in t

![Task Arguments](../images/custom-task-arguments.png)


### System wide tasks

Platform administrators are able to register system wide tasks. These tasks will appear for all environments, subject to the user's permission to invoke them.

Creating a system wide task is almost exactly the same as other task types, with two exceptions.

First, you set the `systemWide: true` field in your `addAdvancedTaskDefinition` mutation.

Second, you make sure you have *not* specified `groupName`, `project`, or `environment` - which would defeat the purpose, since these fields are used to target specific contexts.


### Confirmation

When the `confirmationText` field has text, it will be displayed with a confirmation modal in the UI before the user is able to run the task.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function(knex) {
return knex.schema
.alterTable('advanced_task_definition', (table) => {
table.boolean('system_wide').notNullable().defaultTo(0);
})
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function(knex) {
return knex.schema
.alterTable('advanced_task_definition', (table) => {
table.dropColumn('system_wide');
})
};
3 changes: 2 additions & 1 deletion services/api/src/resources/task/models/taskRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface AdvancedTaskDefinitionInterface {
deployTokenInjection?: boolean;
projectKeyInjection?: boolean;
adminOnlyView?: boolean;
systemWide?: boolean;
advancedTaskDefinitionArguments?: Partial<AdvancedTaskDefinitionArguments>;
}

Expand All @@ -65,7 +66,7 @@ export const getAdvancedTaskDefinitionType = (taskDef:AdvancedTaskDefinitionInte
}

export const isAdvancedTaskDefinitionSystemLevelTask = (taskDef:AdvancedTaskDefinitionInterface): boolean => {
return taskDef.project == null && taskDef.environment == null && taskDef.groupName == null;
return taskDef.systemWide == true;
}

export const doesAdvancedTaskDefinitionNeedAdminRights = (taskDef:AdvancedTaskDefinitionInterface): boolean => {
Expand Down
16 changes: 15 additions & 1 deletion services/api/src/resources/task/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const Sql = {
deploy_token_injection,
project_key_injection,
admin_only_view,
system_wide,
}: {
id: number,
name: string,
Expand All @@ -118,6 +119,7 @@ export const Sql = {
deploy_token_injection: boolean,
project_key_injection: boolean,
admin_only_view: boolean,
system_wide: boolean,
}) =>
knex('advanced_task_definition')
.insert({
Expand All @@ -137,6 +139,7 @@ export const Sql = {
deploy_token_injection,
project_key_injection,
admin_only_view,
system_wide,
})
.toString(),
insertAdvancedTaskDefinitionArgument: ({
Expand Down Expand Up @@ -215,7 +218,7 @@ export const Sql = {
.select(knex.raw(`*, advanced_task_definition.admin_only_view XOR 1 as "show_ui", advanced_task_definition.deploy_token_injection as "admin_task"`)) //use admin_only_view as show_ui for backwards compatability
.where('advanced_task_definition.name', '=', name)
.toString(),
selectAdvancedTaskDefinitionByNameProjectEnvironmentAndGroup:(name: string, project: number, environment: number, group: string) => {
selectAdvancedTaskDefinitionByNameProjectEnvironmentAndGroup:(name: string, project: number, environment: number, group: string, systemWide: boolean = false) => {
let query = knex('advanced_task_definition')
.where('advanced_task_definition.name', '=', name);
if(project) {
Expand All @@ -227,6 +230,9 @@ export const Sql = {
if(group) {
query = query.where('advanced_task_definition.group_name', '=', group)
}
if(systemWide == true) {
query = query.where('advanced_task_definition.system_wide', '=', "1")
}
return query.toString()
},
selectAdvancedTaskDefinitions:() =>
Expand All @@ -248,6 +254,14 @@ export const Sql = {
.select(knex.raw(`*, advanced_task_definition.admin_only_view XOR 1 as "show_ui", advanced_task_definition.deploy_token_injection as "admin_task"`)) //use admin_only_view as show_ui for backwards compatability
.where('group_name', 'in', groups)
.toString(),
selectAdvancedTaskDefinitionsForSystem:() =>
knex('advanced_task_definition')
.select(knex.raw(`*, advanced_task_definition.admin_only_view XOR 1 as "show_ui", advanced_task_definition.deploy_token_injection as "admin_task"`)) //use admin_only_view as show_ui for backwards compatability
.where('environment','is', null)
.andWhere('project','is', null)
.andWhere('group_name','is', null)
.andWhere('system_wide','=','1')
.toString(),
deleteAdvancedTaskDefinition:(id: number) =>
knex('advanced_task_definition')
.where('id', id)
Expand Down
42 changes: 31 additions & 11 deletions services/api/src/resources/task/task_definition_resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,16 @@ export const resolveTasksForEnvironment = async (
Sql.selectAdvancedTaskDefinitionsForGroups(projectGroupsFiltered)
);

const systemWideRows = await query(
sqlClientPool,
Sql.selectAdvancedTaskDefinitionsForSystem()
);

//@ts-ignore
let rows = R.uniqBy(o => o.name, R.concat(R.concat(environmentRows, projectRows), groupRows));
let totalRows = R.concat(R.concat(R.concat(environmentRows, projectRows), groupRows), systemWideRows);

//@ts-ignore
let rows = R.uniqBy(o => o.name, totalRows);

//now we filter the permissions
const currentUsersPermissionForProject = await currentUsersAdvancedTaskRBACRolesForProject(
Expand Down Expand Up @@ -173,6 +181,7 @@ export const resolveTasksForEnvironment = async (
//@ts-ignore
rows[i].advancedTaskDefinitionArguments = processedArgs;
}

return rows;
};

Expand Down Expand Up @@ -241,6 +250,7 @@ export const addAdvancedTaskDefinition = async (
deployTokenInjection,
projectKeyInjection,
adminOnlyView,
systemWide,
} = input;

const atb = advancedTaskToolbox.advancedTaskFunctions(
Expand All @@ -256,6 +266,7 @@ export const addAdvancedTaskDefinition = async (
input.deployTokenInjection = input.deployTokenInjection || false;
input.projectKeyInjection = input.projectKeyInjection || false;
input.adminOnlyView = input.adminOnlyView || false;
input.systemWide = input.systemWide || false;

await checkAdvancedTaskPermissions(input, hasPermission, models, projectObj);

Expand All @@ -267,6 +278,17 @@ export const addAdvancedTaskDefinition = async (
throw Error("Only one of `environment`, `project`, or `groupName` should be set when creating a custom task.");
}

// If we have a system wide task, there should be no group/env/project
if(systemWide == true && (environment || groupName || project)) {
throw Error("When creating a system wide task, you cannot specify a group/project/environment");
}

if(systemWide == false && (environment == null && groupName == null && project == null)) {
throw Error("If you're trying to define a system wide task, set 'systemWide' to true, else set a target");
}




//let's see if there's already an advanced task definition with this name ...
// Note: this will all be scoped to either System, group, project, or environment
Expand All @@ -277,7 +299,8 @@ export const addAdvancedTaskDefinition = async (
name,
project,
environment,
groupName
groupName,
systemWide
)
);
let taskDef = R.prop(0, rows);
Expand Down Expand Up @@ -314,6 +337,7 @@ export const addAdvancedTaskDefinition = async (
return taskDef;
}


const { insertId } = await query(
sqlClientPool,
Sql.insertAdvancedTaskDefinition({
Expand All @@ -333,6 +357,7 @@ export const addAdvancedTaskDefinition = async (
deploy_token_injection: deployTokenInjection,
project_key_injection: projectKeyInjection,
admin_only_view: adminOnlyView,
system_wide: systemWide,
})
);

Expand Down Expand Up @@ -619,7 +644,7 @@ export const invokeRegisteredTask = async (
projectKeyInjection: task.projectKeyInjection,
adminOnlyView: task.adminOnlyView,
remoteId: undefined,
execute: true
execute: true,
});

return advancedTaskData;
Expand Down Expand Up @@ -731,9 +756,7 @@ const getAdvancedTaskTarget = advancedTask => {
} else if (advancedTask.groupName != null) {
return AdvancedTaskDefinitionTarget.Group;
} else {
//Currently, we don't support environment level tasks
throw Error('Images and System Wide Tasks are not yet supported');
// return AdvancedTaskDefinitionTarget.Environment
return AdvancedTaskDefinitionTarget.SystemWide;
}
};

Expand All @@ -749,7 +772,7 @@ function validateAdvancedTaskDefinitionData(input: any, image: any, command: any
break;
case AdvancedTaskDefinitionType.command:
if (!command || 0 === command.length) {
throw new Error('Unable to create Advanced task definition');
throw new Error('Unable to create Advanced task definition - no command provided');
}
break;
default:
Expand All @@ -763,10 +786,7 @@ function validateAdvancedTaskDefinitionData(input: any, image: any, command: any

async function checkAdvancedTaskPermissions(input:AdvancedTaskDefinitionInterface, hasPermission: any, models: any, projectObj: any) {
if (isAdvancedTaskDefinitionSystemLevelTask(input)) {
//if they pass this, they can do basically anything
//In the first release, we're not actually supporting this
//TODO: add checks once images are officially supported - for now, throw an error
throw Error('Adding Images and System Wide Tasks are not yet supported');
hasPermission('advanced_task', 'create:advanced');
} else if (getAdvancedTaskDefinitionType(input) == AdvancedTaskDefinitionType.image || input.deployTokenInjection != false || input.adminOnlyView != false) {
//We're only going to allow administrators to add these for now ...
await hasPermission('advanced_task', 'create:advanced');
Expand Down
17 changes: 10 additions & 7 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const typeDefs = gql`
groupName: String
environment: Int
project: Int
systemWide: Boolean
permission: TaskPermission
deployTokenInjection: Boolean
projectKeyInjection: Boolean
Expand All @@ -178,6 +179,7 @@ const typeDefs = gql`
groupName: String
environment: Int
project: Int
systemWide: Boolean
permission: TaskPermission
deployTokenInjection: Boolean
projectKeyInjection: Boolean
Expand Down Expand Up @@ -1678,6 +1680,7 @@ const typeDefs = gql`
deployTokenInjection: Boolean
projectKeyInjection: Boolean
adminOnlyView: Boolean
systemWide: Boolean
}
input UpdateAdvancedTaskDefinitionInput {
Expand Down Expand Up @@ -2384,19 +2387,19 @@ const typeDefs = gql`
addWorkflow(input: AddWorkflowInput!): Workflow
updateWorkflow(input: UpdateWorkflowInput): Workflow
deleteWorkflow(input: DeleteWorkflowInput!): String
taskDrushArchiveDump(environment: Int!): Task
taskDrushSqlDump(environment: Int!): Task
taskDrushCacheClear(environment: Int!): Task
taskDrushCron(environment: Int!): Task
taskDrushArchiveDump(environment: Int!): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushSqlDump(environment: Int!): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushCacheClear(environment: Int!): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushCron(environment: Int!): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushSqlSync(
sourceEnvironment: Int!
destinationEnvironment: Int!
): Task
): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushRsyncFiles(
sourceEnvironment: Int!
destinationEnvironment: Int!
): Task
taskDrushUserLogin(environment: Int!): Task
): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
taskDrushUserLogin(environment: Int!): Task @deprecated(reason: "This task will be removed in a future release. See https://github.com/uselagoon/lagoon/blob/main/DEPRECATIONS.md for alternatives if you use it")
deleteTask(input: DeleteTaskInput!): String
updateTask(input: UpdateTaskInput): Task
cancelTask(input: CancelTaskInput!): String
Expand Down

0 comments on commit 9f2e8ae

Please sign in to comment.