Skip to content

Commit 873623c

Browse files
committed
feat: support idling and unidling from api with idled status on environment
1 parent 7634d83 commit 873623c

File tree

9 files changed

+294
-8
lines changed

9 files changed

+294
-8
lines changed

node-packages/commons/src/tasks.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,9 +1163,7 @@ export const createMiscTask = async function(taskData: any) {
11631163
data: { project }
11641164
} = taskData;
11651165

1166-
// handle any controller based misc tasks
11671166
let updatedKey = `deploytarget:${key}`;
1168-
let taskId = 'misc-kubernetes';
11691167
// determine the deploy target (openshift/kubernetes) for the task to go to
11701168
// we get this from the environment
11711169
const result = await getOpenShiftInfoForEnvironment(taskData.data.environment.id);
@@ -1296,6 +1294,14 @@ export const createMiscTask = async function(taskData: any) {
12961294
// build cancellation is just a standard unmodified message
12971295
miscTaskData.misc = taskData.data.build
12981296
break;
1297+
case 'deploytarget:environment:idling':
1298+
// environment idling is used to handle idling or unidling of an an environment
1299+
miscTaskData.idling = taskData.data.idling
1300+
break;
1301+
case 'deploytarget:environment:service':
1302+
// environment service is used to handle stop, start, or restarting of a service in an environment
1303+
miscTaskData.lagoonService = taskData.data.lagoonService
1304+
break;
12991305
default:
13001306
miscTaskData.misc = taskData.data.build
13011307
break;

services/actions-handler/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.23
44

55
require (
66
github.com/cheshir/go-mq/v2 v2.0.1
7-
github.com/uselagoon/machinery v0.0.33
7+
github.com/uselagoon/machinery v0.0.35-0.20250519034121-4b2dbc04d43c
88
gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e
99
)
1010

services/actions-handler/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,8 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
842842
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
843843
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
844844
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
845-
github.com/uselagoon/machinery v0.0.33 h1:y+otZ5WC4knqpTiV80YbaXY8U3zbCVenf3Gu0Buc8m0=
846-
github.com/uselagoon/machinery v0.0.33/go.mod h1:G0ujppuNR0BrtAnlmH8xDb9TDfayb4A36aeo0DYg7fQ=
845+
github.com/uselagoon/machinery v0.0.35-0.20250519034121-4b2dbc04d43c h1:EHpPToqjyIUba8ahE9se5Io5orZR9mj4Ycz2DARYQCU=
846+
github.com/uselagoon/machinery v0.0.35-0.20250519034121-4b2dbc04d43c/go.mod h1:G0ujppuNR0BrtAnlmH8xDb9TDfayb4A36aeo0DYg7fQ=
847847
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
848848
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
849849
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"time"
10+
11+
mq "github.com/cheshir/go-mq/v2"
12+
"github.com/uselagoon/machinery/api/lagoon"
13+
lclient "github.com/uselagoon/machinery/api/lagoon/client"
14+
"github.com/uselagoon/machinery/api/schema"
15+
"github.com/uselagoon/machinery/utils/jwt"
16+
)
17+
18+
type Idled struct {
19+
Idled bool `json:"idled"`
20+
}
21+
22+
func (m *Messenger) handleIdling(ctx context.Context, messageQueue *mq.MessageQueue, message *schema.LagoonMessage, messageID string) error {
23+
prefix := fmt.Sprintf("(messageid:%s) %s: ", messageID, message.Namespace)
24+
log.Println(fmt.Sprintf("%sreceived idling environment status update", prefix))
25+
// generate a lagoon token with a expiry of 60 seconds from now
26+
token, err := jwt.GenerateAdminToken(m.LagoonAPI.TokenSigningKey, m.LagoonAPI.JWTAudience, m.LagoonAPI.JWTSubject, m.LagoonAPI.JWTIssuer, time.Now().Unix(), 60)
27+
if err != nil {
28+
// the token wasn't generated
29+
if m.EnableDebug {
30+
log.Println(fmt.Sprintf("ERROR: unable to generate token: %v", err))
31+
}
32+
return nil
33+
}
34+
// set up a lagoon client for use in the following process
35+
l := lclient.New(m.LagoonAPI.Endpoint, "actions-handler", m.LagoonAPI.Version, &token, false)
36+
var environmentID uint
37+
// determine the environment id from the message
38+
if message.Meta.ProjectID == nil && message.Meta.EnvironmentID == nil {
39+
project, err := lagoon.GetMinimalProjectByName(ctx, message.Meta.Project, l)
40+
if err != nil {
41+
// send the log to the lagoon-logs exchange to be processed
42+
m.toLagoonLogs(messageQueue, map[string]interface{}{
43+
"severity": "error",
44+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"),
45+
"meta": project,
46+
"message": err.Error(),
47+
})
48+
if m.EnableDebug {
49+
log.Println(fmt.Sprintf("%sERROR: unable to get project - %v", prefix, err))
50+
}
51+
return err
52+
}
53+
environment, err := lagoon.GetEnvironmentByName(ctx, message.Meta.Environment, project.ID, l)
54+
if err != nil {
55+
// send the log to the lagoon-logs exchange to be processed
56+
m.toLagoonLogs(messageQueue, map[string]interface{}{
57+
"severity": "error",
58+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"),
59+
"meta": project,
60+
"message": err.Error(),
61+
})
62+
if m.EnableDebug {
63+
log.Println(fmt.Sprintf("%sERROR: unable to get environment - %v", prefix, err))
64+
}
65+
return err
66+
}
67+
environmentID = environment.ID
68+
} else {
69+
// pull the id from the message
70+
environmentID = *message.Meta.EnvironmentID
71+
}
72+
decodeData, _ := base64.StdEncoding.DecodeString(message.Meta.AdvancedData)
73+
idled := &Idled{}
74+
json.Unmarshal(decodeData, idled)
75+
updateEnvironmentPatch := schema.UpdateEnvironmentPatchInput{
76+
Idled: &idled.Idled,
77+
}
78+
updateEnvironment, err := lagoon.UpdateEnvironment(ctx, environmentID, updateEnvironmentPatch, l)
79+
if err != nil {
80+
// send the log to the lagoon-logs exchange to be processed
81+
m.toLagoonLogs(messageQueue, map[string]interface{}{
82+
"severity": "error",
83+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateDeployment"),
84+
"meta": updateEnvironment,
85+
"message": err.Error(),
86+
})
87+
if m.EnableDebug {
88+
log.Println(fmt.Sprintf("%sERROR: unable to update environment - %v", prefix, err))
89+
}
90+
return err
91+
}
92+
log.Println(fmt.Sprintf("%supdated environment", prefix))
93+
return nil
94+
}

services/actions-handler/handler/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ func (m *Messenger) Consumer() {
137137
err = m.handleRemoval(ctx, messageQueue, logMsg, messageID)
138138
case "task":
139139
err = m.handleTask(ctx, messageQueue, logMsg, messageID)
140+
case "idling":
141+
err = m.handleIdling(ctx, messageQueue, logMsg, messageID)
140142
}
141143
// 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
142144
// skipping this means the message will remain in the queue
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = async function(knex) {
6+
return knex.schema
7+
.alterTable('environment', (table) => {
8+
table.boolean('idled').notNullable().defaultTo(0);
9+
})
10+
};
11+
12+
/**
13+
* @param { import("knex").Knex } knex
14+
* @returns { Promise<void> }
15+
*/
16+
exports.down = async function(knex) {
17+
return knex.schema
18+
.alterTable('environment', (table) => {
19+
table.dropColumn('idled');
20+
})
21+
};

services/api/src/resolvers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const {
128128
getEnvironmentByServiceId,
129129
getServiceContainersByServiceId,
130130
deleteEnvironmentService,
131+
environmentIdling,
131132
} = require('./resources/environment/resolvers');
132133

133134
const {
@@ -728,6 +729,7 @@ const resolvers = {
728729
deleteEnvironmentService,
729730
addPlatformRoleToUser,
730731
removePlatformRoleFromUser,
732+
environmentIdling,
731733
},
732734
Subscription: {
733735
backupChanged: backupSubscriber,

services/api/src/resources/environment/resolvers.ts

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import * as R from 'ramda';
22
import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger';
3-
import { createRemoveTask, seedNamespace } from '@lagoon/commons/dist/tasks';
3+
import {
4+
createRemoveTask,
5+
createMiscTask,
6+
seedNamespace
7+
} from '@lagoon/commons/dist/tasks';
48
import { ResolverFn } from '../';
59
import { logger } from '../../loggers/logger';
610
import { isPatchEmpty, query, knex } from '../../util/db';
@@ -745,7 +749,8 @@ export const updateEnvironment: ResolverFn = async (
745749
route: input.patch.route,
746750
routes: input.patch.routes,
747751
autoIdle: input.patch.autoIdle,
748-
created: input.patch.created
752+
created: input.patch.created,
753+
idled: input.patch.idled
749754
}
750755
})
751756
);
@@ -787,7 +792,8 @@ export const updateEnvironment: ResolverFn = async (
787792
route: input.patch.route,
788793
routes: input.patch.routes,
789794
autoIdle: input.patch.autoIdle,
790-
created: input.patch.created
795+
created: input.patch.created,
796+
idled: input.patch.idled
791797
},
792798
data: withK8s,
793799
...auditLog,
@@ -1077,3 +1083,153 @@ export const getServiceContainersByServiceId: ResolverFn = async (
10771083
);
10781084
return await rows;
10791085
};
1086+
1087+
export const environmentIdling = async (
1088+
root,
1089+
input,
1090+
{ sqlClientPool, hasPermission, userActivityLogger }
1091+
) => {
1092+
const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id);
1093+
1094+
if (!environment) {
1095+
throw new Error(
1096+
'Invalid environment ID'
1097+
);
1098+
}
1099+
1100+
await hasPermission('environment', 'view', {
1101+
project: environment.project
1102+
});
1103+
1104+
// don't try idle if the environment is already idled or unidled
1105+
if (environment.idled && input.idle) {
1106+
throw new Error(
1107+
`environment is already idled`
1108+
);
1109+
}
1110+
if (!environment.idled && !input.idle) {
1111+
throw new Error(
1112+
`environment is already unidled`
1113+
);
1114+
}
1115+
1116+
const project = await projectHelpers(sqlClientPool).getProjectById(
1117+
environment.project
1118+
);
1119+
1120+
await hasPermission('deployment', 'cancel', {
1121+
project: project.id
1122+
});
1123+
1124+
const data = {
1125+
environment,
1126+
project,
1127+
idling: {
1128+
idle: input.idle,
1129+
forceScale: input.disableAutomaticUnidling
1130+
}
1131+
};
1132+
1133+
userActivityLogger(`User requested environment idling for '${environment.name}'`, {
1134+
project: '',
1135+
event: 'api:idleEnvironment',
1136+
payload: {
1137+
project: project.name,
1138+
environment: environment.name,
1139+
idle: input.idle,
1140+
disableAutomaticUnidling: input.disableAutomaticUnidling,
1141+
}
1142+
});
1143+
1144+
try {
1145+
await createMiscTask({ key: 'environment:idling', data });
1146+
return 'success';
1147+
} catch (error) {
1148+
sendToLagoonLogs(
1149+
'error',
1150+
'',
1151+
'',
1152+
'api:idleEnvironment',
1153+
{ environment: environment.id },
1154+
`Environment idle attempt possibly failed, reason: ${error}`
1155+
);
1156+
throw new Error(
1157+
error.message
1158+
);
1159+
}
1160+
};
1161+
1162+
export const environmentService = async (
1163+
root,
1164+
input,
1165+
{ sqlClientPool, hasPermission, userActivityLogger }
1166+
) => {
1167+
const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id);
1168+
1169+
if (!environment) {
1170+
throw new Error(
1171+
'Invalid environment ID'
1172+
);
1173+
}
1174+
1175+
await hasPermission('environment', 'view', {
1176+
project: environment.project
1177+
});
1178+
1179+
// don't try idle if the environment is already idled or unidled
1180+
if (environment.idled && input.idle) {
1181+
throw new Error(
1182+
`environment is already idled`
1183+
);
1184+
}
1185+
if (!environment.idled && !input.idle) {
1186+
throw new Error(
1187+
`environment is already unidled`
1188+
);
1189+
}
1190+
1191+
const project = await projectHelpers(sqlClientPool).getProjectById(
1192+
environment.project
1193+
);
1194+
1195+
await hasPermission('deployment', 'cancel', {
1196+
project: project.id
1197+
});
1198+
1199+
const data = {
1200+
environment,
1201+
project,
1202+
lagoonService: {
1203+
name: input.serviceName,
1204+
state: input.state
1205+
}
1206+
};
1207+
1208+
userActivityLogger(`User requested environment idling for '${environment.name}'`, {
1209+
project: '',
1210+
event: 'api:idleEnvironment',
1211+
payload: {
1212+
project: project.name,
1213+
environment: environment.name,
1214+
idle: input.idle,
1215+
disableAutomaticUnidling: input.disableAutomaticUnidling,
1216+
}
1217+
});
1218+
1219+
try {
1220+
await createMiscTask({ key: 'environment:idling', data });
1221+
return 'success';
1222+
} catch (error) {
1223+
sendToLagoonLogs(
1224+
'error',
1225+
'',
1226+
'',
1227+
'api:idleEnvironment',
1228+
{ environment: environment.id },
1229+
`Environment idle attempt possibly failed, reason: ${error}`
1230+
);
1231+
throw new Error(
1232+
error.message
1233+
);
1234+
}
1235+
};

services/api/src/typeDefs.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,10 @@ const typeDefs = gql`
879879
openshiftProjectPattern: String @deprecated(reason: "No longer in use")
880880
kubernetes: Kubernetes
881881
kubernetesNamespacePattern: String @deprecated(reason: "No longer in use")
882+
"""
883+
Is the environment currently idled
884+
"""
885+
idled: Boolean
882886
}
883887
884888
type EnvironmentHitsMonth {
@@ -2582,6 +2586,7 @@ const typeDefs = gql`
25822586
deleteEnvironmentService(input: DeleteEnvironmentServiceInput!): String
25832587
addPlatformRoleToUser(user: UserInput!, role: PlatformRole!): User
25842588
removePlatformRoleFromUser(user: UserInput!, role: PlatformRole!): User
2589+
environmentIdling(id: Int!, idle: Boolean!, disableAutomaticUnidling: Boolean): String
25852590
}
25862591
25872592
type Subscription {

0 commit comments

Comments
 (0)