From cf6829a8b885b8d9a1495ec8530d0244ec3e410f Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Fri, 15 Dec 2023 14:11:37 +1100 Subject: [PATCH] feat: support idling and unidling from api with idled status on environment --- node-packages/commons/src/tasks.ts | 771 ++++++++---------- services/actions-handler/go.mod | 2 +- services/actions-handler/go.sum | 2 + .../handler/controller_idling.go | 85 ++ services/actions-handler/handler/handler.go | 2 + ...0240226000000_environment_idling_status.js | 21 + services/api/src/resolvers.js | 4 +- .../src/resources/environment/resolvers.ts | 161 +++- services/api/src/typeDefs.js | 5 + 9 files changed, 630 insertions(+), 423 deletions(-) create mode 100644 services/actions-handler/handler/controller_idling.go create mode 100644 services/api/database/migrations/20240226000000_environment_idling_status.js diff --git a/node-packages/commons/src/tasks.ts b/node-packages/commons/src/tasks.ts index d45cabc1de..15b1356f94 100644 --- a/node-packages/commons/src/tasks.ts +++ b/node-packages/commons/src/tasks.ts @@ -802,104 +802,90 @@ export const createDeployTask = async function(deployData: any) { // production_environment: 'master', // environments: [ { name: 'develop', environment_type: 'development' }, [Object] ] } } - if (typeof project.activeSystemsDeploy === 'undefined') { - throw new UnknownActiveSystem( - `No active system for tasks 'deploy' in for project ${projectName}` + // we want to limit production environments, without making it configurable currently + var productionEnvironmentsLimit = 2; + + // we want to make sure we can deploy the `production` env, and also the env defined as standby + if ( + environments.project.productionEnvironment === branchName || + environments.project.standbyProductionEnvironment === branchName + ) { + // get a list of production environments + const prod_environments = environments.project.environments + .filter(e => e.environmentType === 'production') + .map(e => e.name); + logger.debug( + `projectName: ${projectName}, branchName: ${branchName}, existing environments are ${prod_environments}` ); - } - switch (project.activeSystemsDeploy) { - case 'lagoon_controllerBuildDeploy': - // we want to limit production environments, without making it configurable currently - var productionEnvironmentsLimit = 2; - - // we want to make sure we can deploy the `production` env, and also the env defined as standby - if ( - environments.project.productionEnvironment === branchName || - environments.project.standbyProductionEnvironment === branchName - ) { - // get a list of production environments - const prod_environments = environments.project.environments - .filter(e => e.environmentType === 'production') - .map(e => e.name); + if (prod_environments.length >= productionEnvironmentsLimit) { + if (prod_environments.find(i => i === branchName)) { logger.debug( - `projectName: ${projectName}, branchName: ${branchName}, existing environments are ${prod_environments}` + `projectName: ${projectName}, branchName: ${branchName}, environment already exists, no environment limits considered` ); - - if (prod_environments.length >= productionEnvironmentsLimit) { - if (prod_environments.find(i => i === branchName)) { - logger.debug( - `projectName: ${projectName}, branchName: ${branchName}, environment already exists, no environment limits considered` - ); - } else { - throw new EnvironmentLimit( - `'${branchName}' would exceed the configured limit of ${productionEnvironmentsLimit} production environments for project ${projectName}` - ); - } - } } else { - // get a list of non-production environments - const dev_environments = environments.project.environments - .filter(e => e.environmentType === 'development') - .map(e => e.name); - logger.debug( - `projectName: ${projectName}, branchName: ${branchName}, existing environments are ${dev_environments}` + throw new EnvironmentLimit( + `'${branchName}' would exceed the configured limit of ${productionEnvironmentsLimit} production environments for project ${projectName}` ); - - if ( - environments.project.developmentEnvironmentsLimit !== null && - dev_environments.length >= - environments.project.developmentEnvironmentsLimit - ) { - if (dev_environments.find(i => i === branchName)) { - logger.debug( - `projectName: ${projectName}, branchName: ${branchName}, environment already exists, no environment limits considered` - ); - } else { - throw new EnvironmentLimit( - `'${branchName}' would exceed the configured limit of ${environments.project.developmentEnvironmentsLimit} development environments for project ${projectName}` - ); - } - } } + } + } else { + // get a list of non-production environments + const dev_environments = environments.project.environments + .filter(e => e.environmentType === 'development') + .map(e => e.name); + logger.debug( + `projectName: ${projectName}, branchName: ${branchName}, existing environments are ${dev_environments}` + ); - if (type === 'branch') { - // use deployTargetBranches function to handle - let lagoonData = { - projectId: environments.project.id, - projectName, - branchName, - project, - deployData - } - try { - let result = deployTargetBranches(lagoonData) - return result - } catch (error) { - throw error - } - } else if (type === 'pullrequest') { - // use deployTargetPullrequest function to handle - let lagoonData = { - projectId: environments.project.id, - projectName, - branchName, - project, - pullrequestTitle, - deployData - } - try { - let result = deployTargetPullrequest(lagoonData) - return result - } catch (error) { - throw error - } + if ( + environments.project.developmentEnvironmentsLimit !== null && + dev_environments.length >= + environments.project.developmentEnvironmentsLimit + ) { + if (dev_environments.find(i => i === branchName)) { + logger.debug( + `projectName: ${projectName}, branchName: ${branchName}, environment already exists, no environment limits considered` + ); + } else { + throw new EnvironmentLimit( + `'${branchName}' would exceed the configured limit of ${environments.project.developmentEnvironmentsLimit} development environments for project ${projectName}` + ); } - break; - default: - throw new UnknownActiveSystem( - `Unknown active system '${project.activeSystemsDeploy}' for task 'deploy' in for project ${projectName}` - ); + } + } + + if (type === 'branch') { + // use deployTargetBranches function to handle + let lagoonData = { + projectId: environments.project.id, + projectName, + branchName, + project, + deployData + } + try { + let result = deployTargetBranches(lagoonData) + return result + } catch (error) { + throw error + } + } else if (type === 'pullrequest') { + // use deployTargetPullrequest function to handle + let lagoonData = { + projectId: environments.project.id, + projectName, + branchName, + project, + pullrequestTitle, + deployData + } + try { + let result = deployTargetPullrequest(lagoonData) + return result + } catch (error) { + throw error + } } } @@ -913,25 +899,12 @@ export const createPromoteTask = async function(promoteData: any) { const project = await getActiveSystemForProject(projectName, 'Promote'); - if (typeof project.activeSystemsPromote === 'undefined') { - throw new UnknownActiveSystem( - `No active system for tasks 'deploy' in for project ${projectName}` - ); - } - - switch (project.activeSystemsPromote) { - case 'lagoon_controllerBuildDeploy': - // use deployTargetPromote function to handle - let lagoonData = { - projectId: project.id, - promoteData - } - return deployTargetPromote(lagoonData) - default: - throw new UnknownActiveSystem( - `Unknown active system '${project.activeSystemsPromote}' for task 'deploy' in for project ${projectName}` - ); + // use deployTargetPromote function to handle + let lagoonData = { + projectId: project.id, + promoteData } + return deployTargetPromote(lagoonData) } export const createRemoveTask = async function(removeData: any) { @@ -962,114 +935,95 @@ export const createRemoveTask = async function(removeData: any) { } } - const project = await getActiveSystemForProject(projectName, 'Remove'); + if (type === 'branch') { + let environmentId = 0; + // Check to ensure the environment actually exists. + let foundEnvironment = false; + allEnvironments.project.environments.forEach(function( + environment, + index + ) { + if (environment.name === branch) { + foundEnvironment = true; + environmentId = environment.id; + } + }); - if (typeof project.activeSystemsRemove === 'undefined') { - throw new UnknownActiveSystem( - `No active system for tasks 'remove' in for project ${projectName}` + if (!foundEnvironment) { + logger.debug( + `projectName: ${projectName}, branchName: ${branch}, no environment found.` + ); + throw new NoNeedToRemoveBranch( + 'Branch environment does not exist, no need to remove anything.' + ); + } + // consume the deploytarget from the environment now + const result = await getOpenShiftInfoForEnvironment(environmentId); + const deployTarget = result.environment.openshift.name + logger.debug( + `projectName: ${projectName}, branchName: ${branchName}. Removing branch environment.` ); - } + // use the targetname as the routing key with the action + return sendToLagoonTasks(deployTarget+":remove", removeData); + } else if (type === 'pullrequest') { + // Work out the branch name from the PR number. + let branchName = 'pr-' + pullrequestNumber; + removeData.branchName = 'pr-' + pullrequestNumber; + + let environmentId = 0; + // Check to ensure the environment actually exists. + let foundEnvironment = false; + allEnvironments.project.environments.forEach(function( + environment, + index + ) { + if (environment.name === branchName) { + foundEnvironment = true; + environmentId = environment.id; + } + }); - switch (project.activeSystemsRemove) { - // removed `openshift` and `kubernetes` remove functionality, these services no longer exist in Lagoon - // handle removals using the controllers, send the message to our specific target cluster queue - case 'lagoon_controllerRemove': - if (type === 'branch') { - let environmentId = 0; - // Check to ensure the environment actually exists. - let foundEnvironment = false; - allEnvironments.project.environments.forEach(function( - environment, - index - ) { - if (environment.name === branch) { - foundEnvironment = true; - environmentId = environment.id; - } - }); - - if (!foundEnvironment) { - logger.debug( - `projectName: ${projectName}, branchName: ${branch}, no environment found.` - ); - throw new NoNeedToRemoveBranch( - 'Branch environment does not exist, no need to remove anything.' - ); - } - // consume the deploytarget from the environment now - const result = await getOpenShiftInfoForEnvironment(environmentId); - const deployTarget = result.environment.openshift.name - logger.debug( - `projectName: ${projectName}, branchName: ${branchName}. Removing branch environment.` - ); - // use the targetname as the routing key with the action - return sendToLagoonTasks(deployTarget+":remove", removeData); - } else if (type === 'pullrequest') { - // Work out the branch name from the PR number. - let branchName = 'pr-' + pullrequestNumber; - removeData.branchName = 'pr-' + pullrequestNumber; - - let environmentId = 0; - // Check to ensure the environment actually exists. - let foundEnvironment = false; - allEnvironments.project.environments.forEach(function( - environment, - index - ) { - if (environment.name === branchName) { - foundEnvironment = true; - environmentId = environment.id; - } - }); - - if (!foundEnvironment) { - logger.debug( - `projectName: ${projectName}, pullrequest: ${branchName}, no pullrequest found.` - ); - throw new NoNeedToRemoveBranch( - 'Pull Request environment does not exist, no need to remove anything.' - ); - } - // consume the deploytarget from the environment now - const result = await getOpenShiftInfoForEnvironment(environmentId); - const deployTarget = result.environment.openshift.name - logger.debug( - `projectName: ${projectName}, pullrequest: ${branchName}. Removing pullrequest environment.` - ); - return sendToLagoonTasks(deployTarget+":remove", removeData); - } else if (type === 'promote') { - let environmentId = 0; - // Check to ensure the environment actually exists. - let foundEnvironment = false; - allEnvironments.project.environments.forEach(function( - environment, - index - ) { - if (environment.name === branch) { - foundEnvironment = true; - environmentId = environment.id; - } - }); - - if (!foundEnvironment) { - logger.debug( - `projectName: ${projectName}, branchName: ${branch}, no environment found.` - ); - throw new NoNeedToRemoveBranch( - 'Branch environment does not exist, no need to remove anything.' - ); - } - // consume the deploytarget from the environment now - const result = await getOpenShiftInfoForEnvironment(environmentId); - const deployTarget = result.environment.openshift.name - return sendToLagoonTasks(deployTarget+":remove", removeData); + if (!foundEnvironment) { + logger.debug( + `projectName: ${projectName}, pullrequest: ${branchName}, no pullrequest found.` + ); + throw new NoNeedToRemoveBranch( + 'Pull Request environment does not exist, no need to remove anything.' + ); + } + // consume the deploytarget from the environment now + const result = await getOpenShiftInfoForEnvironment(environmentId); + const deployTarget = result.environment.openshift.name + logger.debug( + `projectName: ${projectName}, pullrequest: ${branchName}. Removing pullrequest environment.` + ); + return sendToLagoonTasks(deployTarget+":remove", removeData); + } else if (type === 'promote') { + let environmentId = 0; + // Check to ensure the environment actually exists. + let foundEnvironment = false; + allEnvironments.project.environments.forEach(function( + environment, + index + ) { + if (environment.name === branch) { + foundEnvironment = true; + environmentId = environment.id; } - break; + }); - default: - throw new UnknownActiveSystem( - `Unknown active system '${project.activeSystemsRemove}' for task 'remove' in for project ${projectName}` + if (!foundEnvironment) { + logger.debug( + `projectName: ${projectName}, branchName: ${branch}, no environment found.` ); + throw new NoNeedToRemoveBranch( + 'Branch environment does not exist, no need to remove anything.' + ); + } + // consume the deploytarget from the environment now + const result = await getOpenShiftInfoForEnvironment(environmentId); + const deployTarget = result.environment.openshift.name + return sendToLagoonTasks(deployTarget+":remove", removeData); } } @@ -1164,25 +1118,11 @@ export const createTaskTask = async function(taskData: any) { taskData.project.organization = organization } - if (typeof projectSystem.activeSystemsTask === 'undefined') { - throw new UnknownActiveSystem( - `No active system for 'task' for project ${project.name}` - ); - } - - switch (projectSystem.activeSystemsTask) { - case 'lagoon_controllerJob': - // since controllers queues are named, we have to send it to the right tasks queue - // do that here by querying which deploytarget the environment uses - const result = await getOpenShiftInfoForEnvironment(taskData.environment.id); - const deployTarget = result.environment.openshift.name - return sendToLagoonTasks(deployTarget+":jobs", taskData); - - default: - throw new UnknownActiveSystem( - `Unknown active system '${projectSystem.activeSystemsTask}' for 'task' for project ${project.name}` - ); - } + // since controllers queues are named, we have to send it to the right tasks queue + // do that here by querying which deploytarget the environment uses + const result = await getOpenShiftInfoForEnvironment(taskData.environment.id); + const deployTarget = result.environment.openshift.name + return sendToLagoonTasks(deployTarget+":jobs", taskData); } export const createMiscTask = async function(taskData: any) { @@ -1191,208 +1131,203 @@ export const createMiscTask = async function(taskData: any) { data: { project } } = taskData; - const data = await getActiveSystemForProject(project.name, 'Misc'); - - let updatedKey = key; - let taskId = ''; - switch (data.activeSystemsMisc) { - case 'lagoon_controllerMisc': - // handle any controller based misc tasks - updatedKey = `deploytarget:${key}`; - taskId = 'misc-kubernetes'; - // determine the deploy target (openshift/kubernetes) for the task to go to - // we get this from the environment - const result = await getOpenShiftInfoForEnvironment(taskData.data.environment.id); - const deployTarget = result.environment.openshift.name - // this is the json structure for sending a misc task to the controller - // there are some additional bits that can be adjusted, and these are done in the switch below on `updatedKey` - var miscTaskData: any = { - misc: {}, - key: updatedKey, - environment: { - name: taskData.data.environment.name, - openshiftProjectName: taskData.data.environment.openshiftProjectName - }, - project: { - name: taskData.data.project.name - }, - task: taskData.data.task, - advancedTask: {} + let updatedKey = `deploytarget:${key}`; + // determine the deploy target (openshift/kubernetes) for the task to go to + // we get this from the environment + const result = await getOpenShiftInfoForEnvironment(taskData.data.environment.id); + const deployTarget = result.environment.openshift.name + // this is the json structure for sending a misc task to the controller + // there are some additional bits that can be adjusted, and these are done in the switch below on `updatedKey` + var miscTaskData: any = { + misc: {}, + key: updatedKey, + environment: { + name: taskData.data.environment.name, + openshiftProjectName: taskData.data.environment.openshiftProjectName + }, + project: { + name: taskData.data.project.name + }, + task: taskData.data.task, + advancedTask: {} + } + switch (updatedKey) { + case 'deploytarget:restic:backup:restore': + // Handle setting up the configuration for a restic restoration task + const randRestoreId = Math.random().toString(36).substring(7); + const restoreName = `restore-${R.slice(0, 7, taskData.data.backup.backupId)}-${randRestoreId}`; + // Parse out the baasBucketName for any migrated projects + // check if the project is configured for a shared baas bucket + let [baasBucketName, shared] = await getBaasBucketName(result.environment.project, result.environment.openshift) + if (shared) { + // if it is a shared bucket, add the repo key to it too for restores + baasBucketName = `${baasBucketName}/baas-${makeSafe(taskData.data.project.name)}` + } + // Handle custom backup configurations + let lagoonBaasCustomBackupEndpoint = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_ENDPOINT" + }) + if (lagoonBaasCustomBackupEndpoint) { + lagoonBaasCustomBackupEndpoint = lagoonBaasCustomBackupEndpoint.value + } + let lagoonBaasCustomBackupBucket = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_BUCKET" + }) + if (lagoonBaasCustomBackupBucket) { + lagoonBaasCustomBackupBucket = lagoonBaasCustomBackupBucket.value + } + let lagoonBaasCustomBackupAccessKey = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_ACCESS_KEY" + }) + if (lagoonBaasCustomBackupAccessKey) { + lagoonBaasCustomBackupAccessKey = lagoonBaasCustomBackupAccessKey.value + } + let lagoonBaasCustomBackupSecretKey = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_SECRET_KEY" + }) + if (lagoonBaasCustomBackupSecretKey) { + lagoonBaasCustomBackupSecretKey = lagoonBaasCustomBackupSecretKey.value } - switch (updatedKey) { - case 'deploytarget:restic:backup:restore': - // Handle setting up the configuration for a restic restoration task - const randRestoreId = Math.random().toString(36).substring(7); - const restoreName = `restore-${R.slice(0, 7, taskData.data.backup.backupId)}-${randRestoreId}`; - // Parse out the baasBucketName for any migrated projects - // check if the project is configured for a shared baas bucket - let [baasBucketName, shared] = await getBaasBucketName(result.environment.project, result.environment.openshift) - if (shared) { - // if it is a shared bucket, add the repo key to it too for restores - baasBucketName = `${baasBucketName}/baas-${makeSafe(taskData.data.project.name)}` - } - // Handle custom backup configurations - let lagoonBaasCustomBackupEndpoint = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_ENDPOINT" - }) - if (lagoonBaasCustomBackupEndpoint) { - lagoonBaasCustomBackupEndpoint = lagoonBaasCustomBackupEndpoint.value - } - let lagoonBaasCustomBackupBucket = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_BUCKET" - }) - if (lagoonBaasCustomBackupBucket) { - lagoonBaasCustomBackupBucket = lagoonBaasCustomBackupBucket.value - } - let lagoonBaasCustomBackupAccessKey = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_ACCESS_KEY" - }) - if (lagoonBaasCustomBackupAccessKey) { - lagoonBaasCustomBackupAccessKey = lagoonBaasCustomBackupAccessKey.value - } - let lagoonBaasCustomBackupSecretKey = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_BACKUP_SECRET_KEY" - }) - if (lagoonBaasCustomBackupSecretKey) { - lagoonBaasCustomBackupSecretKey = lagoonBaasCustomBackupSecretKey.value - } - let backupS3Config = {} - if (lagoonBaasCustomBackupEndpoint && lagoonBaasCustomBackupBucket && lagoonBaasCustomBackupAccessKey && lagoonBaasCustomBackupSecretKey) { - backupS3Config = { - endpoint: lagoonBaasCustomBackupEndpoint, - bucket: lagoonBaasCustomBackupBucket, - accessKeyIDSecretRef: { - name: "lagoon-baas-custom-backup-credentials", - key: "access-key" - }, - secretAccessKeySecretRef: { - name: "lagoon-baas-custom-backup-credentials", - key: "secret-key" - } - } - } else { - backupS3Config = { - bucket: baasBucketName ? baasBucketName : `baas-${makeSafe(taskData.data.project.name)}` - } + let backupS3Config = {} + if (lagoonBaasCustomBackupEndpoint && lagoonBaasCustomBackupBucket && lagoonBaasCustomBackupAccessKey && lagoonBaasCustomBackupSecretKey) { + backupS3Config = { + endpoint: lagoonBaasCustomBackupEndpoint, + bucket: lagoonBaasCustomBackupBucket, + accessKeyIDSecretRef: { + name: "lagoon-baas-custom-backup-credentials", + key: "access-key" + }, + secretAccessKeySecretRef: { + name: "lagoon-baas-custom-backup-credentials", + key: "secret-key" } + } + } else { + backupS3Config = { + bucket: baasBucketName ? baasBucketName : `baas-${makeSafe(taskData.data.project.name)}` + } + } - // Handle custom restore configurations - let lagoonBaasCustomRestoreEndpoint = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_ENDPOINT" - }) - if (lagoonBaasCustomRestoreEndpoint) { - lagoonBaasCustomRestoreEndpoint = lagoonBaasCustomRestoreEndpoint.value - } - let lagoonBaasCustomRestoreBucket = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_BUCKET" - }) - if (lagoonBaasCustomRestoreBucket) { - lagoonBaasCustomRestoreBucket = lagoonBaasCustomRestoreBucket.value - } - let lagoonBaasCustomRestoreAccessKey = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_ACCESS_KEY" - }) - if (lagoonBaasCustomRestoreAccessKey) { - lagoonBaasCustomRestoreAccessKey = lagoonBaasCustomRestoreAccessKey.value - } - let lagoonBaasCustomRestoreSecretKey = result.environment.project.envVariables.find(obj => { - return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_SECRET_KEY" - }) - if (lagoonBaasCustomRestoreSecretKey) { - lagoonBaasCustomRestoreSecretKey = lagoonBaasCustomRestoreSecretKey.value - } + // Handle custom restore configurations + let lagoonBaasCustomRestoreEndpoint = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_ENDPOINT" + }) + if (lagoonBaasCustomRestoreEndpoint) { + lagoonBaasCustomRestoreEndpoint = lagoonBaasCustomRestoreEndpoint.value + } + let lagoonBaasCustomRestoreBucket = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_BUCKET" + }) + if (lagoonBaasCustomRestoreBucket) { + lagoonBaasCustomRestoreBucket = lagoonBaasCustomRestoreBucket.value + } + let lagoonBaasCustomRestoreAccessKey = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_ACCESS_KEY" + }) + if (lagoonBaasCustomRestoreAccessKey) { + lagoonBaasCustomRestoreAccessKey = lagoonBaasCustomRestoreAccessKey.value + } + let lagoonBaasCustomRestoreSecretKey = result.environment.project.envVariables.find(obj => { + return obj.name === "LAGOON_BAAS_CUSTOM_RESTORE_SECRET_KEY" + }) + if (lagoonBaasCustomRestoreSecretKey) { + lagoonBaasCustomRestoreSecretKey = lagoonBaasCustomRestoreSecretKey.value + } - let restoreS3Config = {} - if (lagoonBaasCustomRestoreEndpoint && lagoonBaasCustomRestoreBucket && lagoonBaasCustomRestoreAccessKey && lagoonBaasCustomRestoreSecretKey) { - restoreS3Config = { - endpoint: lagoonBaasCustomRestoreEndpoint, - bucket: lagoonBaasCustomRestoreBucket, - accessKeyIDSecretRef: { - name: "lagoon-baas-custom-restore-credentials", - key: "access-key" - }, - secretAccessKeySecretRef: { - name: "lagoon-baas-custom-restore-credentials", - key: "secret-key" - } - } + let restoreS3Config = {} + if (lagoonBaasCustomRestoreEndpoint && lagoonBaasCustomRestoreBucket && lagoonBaasCustomRestoreAccessKey && lagoonBaasCustomRestoreSecretKey) { + restoreS3Config = { + endpoint: lagoonBaasCustomRestoreEndpoint, + bucket: lagoonBaasCustomRestoreBucket, + accessKeyIDSecretRef: { + name: "lagoon-baas-custom-restore-credentials", + key: "access-key" + }, + secretAccessKeySecretRef: { + name: "lagoon-baas-custom-restore-credentials", + key: "secret-key" } + } + } - // generate the restore CRD - const restoreConf = restoreConfig(restoreName, taskData.data.backup.backupId, backupS3Config, restoreS3Config) - //logger.info(restoreConf) - // base64 encode it - const restoreBytes = new Buffer(JSON.stringify(restoreConf).replace(/\\n/g, "\n")).toString('base64') - miscTaskData.misc.miscResource = restoreBytes - break; - case 'deploytarget:task:activestandby': - // handle setting up the task configuration for running the active/standby switch - // this uses the `advanced task` system in the controllers - // generate out custom json payload to send to the advanced task - var jsonPayload: any = { - productionEnvironment: taskData.data.productionEnvironment.name, - standbyEnvironment: taskData.data.environment.name, - sourceNamespace: makeSafe(taskData.data.environment.openshiftProjectName), - destinationNamespace: makeSafe(taskData.data.productionEnvironment.openshiftProjectName) - } - // encode it - const jsonPayloadBytes = new Buffer(JSON.stringify(jsonPayload).replace(/\\n/g, "\n")).toString('base64') - // set the task data up - miscTaskData.advancedTask.JSONPayload = jsonPayloadBytes - // use this image to run the task - let taskImage = "" - // choose which task image to use - if (CI == "true") { - taskImage = "172.17.0.1:5000/lagoon/task-activestandby:latest" - } else if (overwriteActiveStandbyTaskImage) { - // allow to overwrite the image we use via OVERWRITE_ACTIVESTANDBY_TASK_IMAGE env variable - taskImage = overwriteActiveStandbyTaskImage - } else { - taskImage = `uselagoon/task-activestandby:${lagoonVersion}` - } - miscTaskData.advancedTask.runnerImage = taskImage - // miscTaskData.advancedTask.runnerImage = "shreddedbacon/runner:latest" - break; - case 'deploytarget:task:advanced': - // inject variables into advanced tasks the same way it is in builds and standard tasks - const [_, envVars, projectVars] = await getTaskProjectEnvironmentVariables( - taskData.data.project.name, - taskData.data.environment.id - ) - miscTaskData.project.variables = { - project: projectVars, - environment: envVars, - } - miscTaskData.advancedTask = taskData.data.advancedTask - break; - case 'deploytarget:task:cancel': - // task cancellation is just a standard unmodified message - miscTaskData.misc = taskData.data.task - break; - case 'deploytarget:build:cancel': - // build cancellation is just a standard unmodified message - miscTaskData.misc = taskData.data.build - break; - default: - miscTaskData.misc = taskData.data.build - break; + // generate the restore CRD + const restoreConf = restoreConfig(restoreName, taskData.data.backup.backupId, backupS3Config, restoreS3Config) + //logger.info(restoreConf) + // base64 encode it + const restoreBytes = new Buffer(JSON.stringify(restoreConf).replace(/\\n/g, "\n")).toString('base64') + miscTaskData.misc.miscResource = restoreBytes + break; + case 'deploytarget:task:activestandby': + // handle setting up the task configuration for running the active/standby switch + // this uses the `advanced task` system in the controllers + // generate out custom json payload to send to the advanced task + var jsonPayload: any = { + productionEnvironment: taskData.data.productionEnvironment.name, + standbyEnvironment: taskData.data.environment.name, + sourceNamespace: makeSafe(taskData.data.environment.openshiftProjectName), + destinationNamespace: makeSafe(taskData.data.productionEnvironment.openshiftProjectName) } - // send the task to the queue - if (project.organization != null) { - const curOrg = await getOrganizationById(project.organization); - const organization = { - name: curOrg.name, - id: curOrg.id, - } - miscTaskData.project.organization = organization + // encode it + const jsonPayloadBytes = new Buffer(JSON.stringify(jsonPayload).replace(/\\n/g, "\n")).toString('base64') + // set the task data up + miscTaskData.advancedTask.JSONPayload = jsonPayloadBytes + // use this image to run the task + let taskImage = "" + // choose which task image to use + if (CI == "true") { + taskImage = "172.17.0.1:5000/lagoon/task-activestandby:latest" + } else if (overwriteActiveStandbyTaskImage) { + // allow to overwrite the image we use via OVERWRITE_ACTIVESTANDBY_TASK_IMAGE env variable + taskImage = overwriteActiveStandbyTaskImage + } else { + taskImage = `uselagoon/task-activestandby:${lagoonVersion}` } - return sendToLagoonTasks(deployTarget+':misc', miscTaskData); + miscTaskData.advancedTask.runnerImage = taskImage + // miscTaskData.advancedTask.runnerImage = "shreddedbacon/runner:latest" + break; + case 'deploytarget:task:advanced': + // inject variables into advanced tasks the same way it is in builds and standard tasks + const [_, envVars, projectVars] = await getTaskProjectEnvironmentVariables( + taskData.data.project.name, + taskData.data.environment.id + ) + miscTaskData.project.variables = { + project: projectVars, + environment: envVars, + } + miscTaskData.advancedTask = taskData.data.advancedTask + break; + case 'deploytarget:task:cancel': + // task cancellation is just a standard unmodified message + miscTaskData.misc = taskData.data.task + break; + case 'deploytarget:build:cancel': + // build cancellation is just a standard unmodified message + miscTaskData.misc = taskData.data.build + break; + case 'deploytarget:environment:idling': + // environment idling is used to handle idling or unidling of an an environment + miscTaskData.idling = taskData.data.idling + break; + case 'deploytarget:environment:service': + // environment service is used to handle stop, start, or restarting of a service in an environment + miscTaskData.lagoonService = taskData.data.lagoonService + break; default: + miscTaskData.misc = taskData.data.build break; } - - return sendToLagoonTasks(taskId, { ...taskData, key: updatedKey }); + // send the task to the queue + if (project.organization != null) { + const curOrg = await getOrganizationById(project.organization); + const organization = { + name: curOrg.name, + id: curOrg.id, + } + miscTaskData.project.organization = organization + } + return sendToLagoonTasks(deployTarget+':misc', miscTaskData); } export const consumeTasks = async function( diff --git a/services/actions-handler/go.mod b/services/actions-handler/go.mod index e64c1868a5..7f77a05c8b 100644 --- a/services/actions-handler/go.mod +++ b/services/actions-handler/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/cheshir/go-mq/v2 v2.0.1 - github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3 + github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e ) diff --git a/services/actions-handler/go.sum b/services/actions-handler/go.sum index 5ba1324719..31637a4bb4 100644 --- a/services/actions-handler/go.sum +++ b/services/actions-handler/go.sum @@ -853,6 +853,8 @@ github.com/uselagoon/machinery v0.0.17-0.20240108050446-30ff0a7df794 h1:2LP/ytk7 github.com/uselagoon/machinery v0.0.17-0.20240108050446-30ff0a7df794/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU= github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3 h1:DYklzy44C1s1a1O6LqAi8RUpuqDzTzJTnW9IRQ8J91k= github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU= +github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab h1:lBAsDSwVaj7l7Bt53yiWlll0mq4ZsEGtj/queCK+2OY= +github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= diff --git a/services/actions-handler/handler/controller_idling.go b/services/actions-handler/handler/controller_idling.go new file mode 100644 index 0000000000..19cb958c2d --- /dev/null +++ b/services/actions-handler/handler/controller_idling.go @@ -0,0 +1,85 @@ +package handler + +import ( + "context" + "fmt" + "log" + "time" + + mq "github.com/cheshir/go-mq/v2" + "github.com/uselagoon/machinery/api/lagoon" + lclient "github.com/uselagoon/machinery/api/lagoon/client" + "github.com/uselagoon/machinery/api/schema" + "github.com/uselagoon/machinery/utils/jwt" +) + +func (m *Messenger) handleIdling(ctx context.Context, messageQueue *mq.MessageQueue, message *schema.LagoonMessage, messageID string) error { + prefix := fmt.Sprintf("(messageid:%s) %s: ", messageID, message.Namespace) + log.Println(fmt.Sprintf("%sreceived idling environment status update", prefix)) + // 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) + if err != nil { + // the token wasn't generated + if m.EnableDebug { + log.Println(fmt.Sprintf("ERROR: unable to generate token: %v", err)) + } + return nil + } + // set up a lagoon client for use in the following process + l := lclient.New(m.LagoonAPI.Endpoint, "actions-handler", &token, false) + var environmentID uint + // determine the environment id from the message + if message.Meta.ProjectID == nil && message.Meta.EnvironmentID == nil { + project, err := lagoon.GetMinimalProjectByName(ctx, message.Meta.Project, l) + if err != nil { + // send the log to the lagoon-logs exchange to be processed + m.toLagoonLogs(messageQueue, map[string]interface{}{ + "severity": "error", + "event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"), + "meta": project, + "message": err.Error(), + }) + if m.EnableDebug { + log.Println(fmt.Sprintf("%sERROR: unable to get project - %v", prefix, err)) + } + return err + } + environment, err := lagoon.GetEnvironmentByName(ctx, message.Meta.Environment, project.ID, l) + if err != nil { + // send the log to the lagoon-logs exchange to be processed + m.toLagoonLogs(messageQueue, map[string]interface{}{ + "severity": "error", + "event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"), + "meta": project, + "message": err.Error(), + }) + if m.EnableDebug { + log.Println(fmt.Sprintf("%sERROR: unable to get environment - %v", prefix, err)) + } + return err + } + environmentID = environment.ID + } else { + // pull the id from the message + environmentID = *message.Meta.EnvironmentID + } + updateEnvironmentPatch := schema.UpdateEnvironmentPatchInput{ + Idled: &message.Idled, + } + updateEnvironment, err := lagoon.UpdateEnvironment(ctx, environmentID, updateEnvironmentPatch, l) + if err != nil { + // send the log to the lagoon-logs exchange to be processed + m.toLagoonLogs(messageQueue, map[string]interface{}{ + "severity": "error", + "event": fmt.Sprintf("actions-handler:%s:failed", "updateDeployment"), + "meta": updateEnvironment, + "message": err.Error(), + }) + if m.EnableDebug { + log.Println(fmt.Sprintf("%sERROR: unable to update environment - %v", prefix, err)) + } + return err + } + log.Println(fmt.Sprintf("%supdated environment", prefix)) + return nil +} diff --git a/services/actions-handler/handler/handler.go b/services/actions-handler/handler/handler.go index 44bfca187f..b7809182af 100644 --- a/services/actions-handler/handler/handler.go +++ b/services/actions-handler/handler/handler.go @@ -142,6 +142,8 @@ func (m *Messenger) Consumer() { err = m.handleRemoval(ctx, messageQueue, logMsg, messageID) case "task": err = m.handleTask(ctx, messageQueue, logMsg, messageID) + case "idling": + err = m.handleIdling(ctx, messageQueue, logMsg, messageID) } // if there aren't any errors, then ack the message, an error indicates that there may have been an issue with the api handling the request // skipping this means the message will remain in the queue diff --git a/services/api/database/migrations/20240226000000_environment_idling_status.js b/services/api/database/migrations/20240226000000_environment_idling_status.js new file mode 100644 index 0000000000..c2f7ec300a --- /dev/null +++ b/services/api/database/migrations/20240226000000_environment_idling_status.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function(knex) { + return knex.schema + .alterTable('environment', (table) => { + table.boolean('idled').notNullable().defaultTo(0); + }) +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function(knex) { + return knex.schema + .alterTable('environment', (table) => { + table.dropColumn('idled'); + }) +}; diff --git a/services/api/src/resolvers.js b/services/api/src/resolvers.js index 8c192b3174..86b6b48fd9 100644 --- a/services/api/src/resolvers.js +++ b/services/api/src/resolvers.js @@ -128,6 +128,7 @@ const { getEnvironmentByServiceId, getServiceContainersByServiceId, deleteEnvironmentService, + environmentIdling, } = require('./resources/environment/resolvers'); const { @@ -730,7 +731,8 @@ const resolvers = { removeUserFromOrganizationGroups, bulkImportProjectsAndGroupsToOrganization, addOrUpdateEnvironmentService, - deleteEnvironmentService + deleteEnvironmentService, + environmentIdling, }, Subscription: { backupChanged: backupSubscriber, diff --git a/services/api/src/resources/environment/resolvers.ts b/services/api/src/resources/environment/resolvers.ts index cab7fb4a9c..2b370678bb 100644 --- a/services/api/src/resources/environment/resolvers.ts +++ b/services/api/src/resources/environment/resolvers.ts @@ -2,7 +2,10 @@ import * as R from 'ramda'; // @ts-ignore import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger'; // @ts-ignore -import { createRemoveTask } from '@lagoon/commons/dist/tasks'; +import { + createRemoveTask, + createMiscTask +} from '@lagoon/commons/dist/tasks'; import { ResolverFn } from '../'; import { logger } from '../../loggers/logger'; import { isPatchEmpty, query, knex } from '../../util/db'; @@ -650,7 +653,8 @@ export const updateEnvironment: ResolverFn = async ( route: input.patch.route, routes: input.patch.routes, autoIdle: input.patch.autoIdle, - created: input.patch.created + created: input.patch.created, + idled: input.patch.idled } }) ); @@ -675,7 +679,8 @@ export const updateEnvironment: ResolverFn = async ( route: input.patch.route, routes: input.patch.routes, autoIdle: input.patch.autoIdle, - created: input.patch.created + created: input.patch.created, + idled: input.patch.idled }, data: withK8s } @@ -930,4 +935,154 @@ export const getServiceContainersByServiceId: ResolverFn = async ( Sql.selectContainersByServiceId(sid) ); return await rows; +} + +export const environmentIdling = async ( + root, + input, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id); + + if (!environment) { + throw new Error( + 'Invalid environment ID' + ); + } + + await hasPermission('environment', 'view', { + project: environment.project + }); + + // don't try idle if the environment is already idled or unidled + if (environment.idled && input.idle) { + throw new Error( + `environment is already idled` + ); + } + if (!environment.idled && !input.idle) { + throw new Error( + `environment is already unidled` + ); + } + + const project = await projectHelpers(sqlClientPool).getProjectById( + environment.project + ); + + await hasPermission('deployment', 'cancel', { + project: project.id + }); + + const data = { + environment, + project, + idling: { + idle: input.idle, + forceScale: input.disableAutomaticUnidling + } + }; + + userActivityLogger(`User requested environment idling for '${environment.name}'`, { + project: '', + event: 'api:idleEnvironment', + payload: { + project: project.name, + environment: environment.name, + idle: input.idle, + disableAutomaticUnidling: input.disableAutomaticUnidling, + } + }); + + try { + await createMiscTask({ key: 'environment:idling', data }); + return 'success'; + } catch (error) { + sendToLagoonLogs( + 'error', + '', + '', + 'api:idleEnvironment', + { environment: environment.id }, + `Environment idle attempt possibly failed, reason: ${error}` + ); + throw new Error( + error.message + ); + } +}; + +export const environmentService = async ( + root, + input, + { sqlClientPool, hasPermission, userActivityLogger } +) => { + const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id); + + if (!environment) { + throw new Error( + 'Invalid environment ID' + ); + } + + await hasPermission('environment', 'view', { + project: environment.project + }); + + // don't try idle if the environment is already idled or unidled + if (environment.idled && input.idle) { + throw new Error( + `environment is already idled` + ); + } + if (!environment.idled && !input.idle) { + throw new Error( + `environment is already unidled` + ); + } + + const project = await projectHelpers(sqlClientPool).getProjectById( + environment.project + ); + + await hasPermission('deployment', 'cancel', { + project: project.id + }); + + const data = { + environment, + project, + lagoonService: { + name: input.serviceName, + state: input.state + } + }; + + userActivityLogger(`User requested environment idling for '${environment.name}'`, { + project: '', + event: 'api:idleEnvironment', + payload: { + project: project.name, + environment: environment.name, + idle: input.idle, + disableAutomaticUnidling: input.disableAutomaticUnidling, + } + }); + + try { + await createMiscTask({ key: 'environment:idling', data }); + return 'success'; + } catch (error) { + sendToLagoonLogs( + 'error', + '', + '', + 'api:idleEnvironment', + { environment: environment.id }, + `Environment idle attempt possibly failed, reason: ${error}` + ); + throw new Error( + error.message + ); + } }; \ No newline at end of file diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 835ec5d846..e5fb7898a2 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -940,6 +940,10 @@ const typeDefs = gql` kubernetes: Kubernetes kubernetesNamespacePattern: String workflows: [Workflow] + """ + Is the environment currently idled + """ + idled: Boolean } type EnvironmentHitsMonth { @@ -2532,6 +2536,7 @@ const typeDefs = gql` bulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput, detachNotification: Boolean): ProjectGroupsToOrganization addOrUpdateEnvironmentService(input: AddEnvironmentServiceInput!): EnvironmentService deleteEnvironmentService(input: DeleteEnvironmentServiceInput!): String + environmentIdling(id: Int!, idle: Boolean!, disableAutomaticUnidling: Boolean): String } type Subscription {