Skip to content

Commit

Permalink
feat: support idling and unidling from api with idled status on envir…
Browse files Browse the repository at this point in the history
…onment
  • Loading branch information
shreddedbacon committed Feb 26, 2024
1 parent 5ac6dae commit cf6829a
Show file tree
Hide file tree
Showing 9 changed files with 630 additions and 423 deletions.
771 changes: 353 additions & 418 deletions node-packages/commons/src/tasks.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion services/actions-handler/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
2 changes: 2 additions & 0 deletions services/actions-handler/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
85 changes: 85 additions & 0 deletions services/actions-handler/handler/controller_idling.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions services/actions-handler/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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('environment', (table) => {
table.boolean('idled').notNullable().defaultTo(0);
})
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function(knex) {
return knex.schema
.alterTable('environment', (table) => {
table.dropColumn('idled');
})
};
4 changes: 3 additions & 1 deletion services/api/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const {
getEnvironmentByServiceId,
getServiceContainersByServiceId,
deleteEnvironmentService,
environmentIdling,
} = require('./resources/environment/resolvers');

const {
Expand Down Expand Up @@ -730,7 +731,8 @@ const resolvers = {
removeUserFromOrganizationGroups,
bulkImportProjectsAndGroupsToOrganization,
addOrUpdateEnvironmentService,
deleteEnvironmentService
deleteEnvironmentService,
environmentIdling,
},
Subscription: {
backupChanged: backupSubscriber,
Expand Down
161 changes: 158 additions & 3 deletions services/api/src/resources/environment/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
}
})
);
Expand All @@ -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
}
Expand Down Expand Up @@ -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
);
}
};
5 changes: 5 additions & 0 deletions services/api/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,10 @@ const typeDefs = gql`
kubernetes: Kubernetes
kubernetesNamespacePattern: String
workflows: [Workflow]
"""
Is the environment currently idled
"""
idled: Boolean
}
type EnvironmentHitsMonth {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit cf6829a

Please sign in to comment.