Skip to content

Commit cf6829a

Browse files
committed
feat: support idling and unidling from api with idled status on environment
1 parent 5ac6dae commit cf6829a

File tree

9 files changed

+630
-423
lines changed

9 files changed

+630
-423
lines changed

node-packages/commons/src/tasks.ts

Lines changed: 353 additions & 418 deletions
Large diffs are not rendered by default.

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.21
44

55
require (
66
github.com/cheshir/go-mq/v2 v2.0.1
7-
github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3
7+
github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab
88
gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e
99
)
1010

services/actions-handler/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,8 @@ github.com/uselagoon/machinery v0.0.17-0.20240108050446-30ff0a7df794 h1:2LP/ytk7
853853
github.com/uselagoon/machinery v0.0.17-0.20240108050446-30ff0a7df794/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU=
854854
github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3 h1:DYklzy44C1s1a1O6LqAi8RUpuqDzTzJTnW9IRQ8J91k=
855855
github.com/uselagoon/machinery v0.0.17-0.20240108054822-78639cc0a1f3/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU=
856+
github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab h1:lBAsDSwVaj7l7Bt53yiWlll0mq4ZsEGtj/queCK+2OY=
857+
github.com/uselagoon/machinery v0.0.17-0.20240226005245-c8fa2fc9f9ab/go.mod h1:Duljjz/3d/7m0jbmF1nVRDTNaMxMr6m+5LkgjiRrQaU=
856858
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
857859
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
858860
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"time"
8+
9+
mq "github.com/cheshir/go-mq/v2"
10+
"github.com/uselagoon/machinery/api/lagoon"
11+
lclient "github.com/uselagoon/machinery/api/lagoon/client"
12+
"github.com/uselagoon/machinery/api/schema"
13+
"github.com/uselagoon/machinery/utils/jwt"
14+
)
15+
16+
func (m *Messenger) handleIdling(ctx context.Context, messageQueue *mq.MessageQueue, message *schema.LagoonMessage, messageID string) error {
17+
prefix := fmt.Sprintf("(messageid:%s) %s: ", messageID, message.Namespace)
18+
log.Println(fmt.Sprintf("%sreceived idling environment status update", prefix))
19+
// generate a lagoon token with a expiry of 60 seconds from now
20+
token, err := jwt.GenerateAdminToken(m.LagoonAPI.TokenSigningKey, m.LagoonAPI.JWTAudience, m.LagoonAPI.JWTSubject, m.LagoonAPI.JWTIssuer, time.Now().Unix(), 60)
21+
if err != nil {
22+
// the token wasn't generated
23+
if m.EnableDebug {
24+
log.Println(fmt.Sprintf("ERROR: unable to generate token: %v", err))
25+
}
26+
return nil
27+
}
28+
// set up a lagoon client for use in the following process
29+
l := lclient.New(m.LagoonAPI.Endpoint, "actions-handler", &token, false)
30+
var environmentID uint
31+
// determine the environment id from the message
32+
if message.Meta.ProjectID == nil && message.Meta.EnvironmentID == nil {
33+
project, err := lagoon.GetMinimalProjectByName(ctx, message.Meta.Project, l)
34+
if err != nil {
35+
// send the log to the lagoon-logs exchange to be processed
36+
m.toLagoonLogs(messageQueue, map[string]interface{}{
37+
"severity": "error",
38+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"),
39+
"meta": project,
40+
"message": err.Error(),
41+
})
42+
if m.EnableDebug {
43+
log.Println(fmt.Sprintf("%sERROR: unable to get project - %v", prefix, err))
44+
}
45+
return err
46+
}
47+
environment, err := lagoon.GetEnvironmentByName(ctx, message.Meta.Environment, project.ID, l)
48+
if err != nil {
49+
// send the log to the lagoon-logs exchange to be processed
50+
m.toLagoonLogs(messageQueue, map[string]interface{}{
51+
"severity": "error",
52+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateEnvironment"),
53+
"meta": project,
54+
"message": err.Error(),
55+
})
56+
if m.EnableDebug {
57+
log.Println(fmt.Sprintf("%sERROR: unable to get environment - %v", prefix, err))
58+
}
59+
return err
60+
}
61+
environmentID = environment.ID
62+
} else {
63+
// pull the id from the message
64+
environmentID = *message.Meta.EnvironmentID
65+
}
66+
updateEnvironmentPatch := schema.UpdateEnvironmentPatchInput{
67+
Idled: &message.Idled,
68+
}
69+
updateEnvironment, err := lagoon.UpdateEnvironment(ctx, environmentID, updateEnvironmentPatch, l)
70+
if err != nil {
71+
// send the log to the lagoon-logs exchange to be processed
72+
m.toLagoonLogs(messageQueue, map[string]interface{}{
73+
"severity": "error",
74+
"event": fmt.Sprintf("actions-handler:%s:failed", "updateDeployment"),
75+
"meta": updateEnvironment,
76+
"message": err.Error(),
77+
})
78+
if m.EnableDebug {
79+
log.Println(fmt.Sprintf("%sERROR: unable to update environment - %v", prefix, err))
80+
}
81+
return err
82+
}
83+
log.Println(fmt.Sprintf("%supdated environment", prefix))
84+
return nil
85+
}

services/actions-handler/handler/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ func (m *Messenger) Consumer() {
142142
err = m.handleRemoval(ctx, messageQueue, logMsg, messageID)
143143
case "task":
144144
err = m.handleTask(ctx, messageQueue, logMsg, messageID)
145+
case "idling":
146+
err = m.handleIdling(ctx, messageQueue, logMsg, messageID)
145147
}
146148
// 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
147149
// 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: 3 additions & 1 deletion
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 {
@@ -730,7 +731,8 @@ const resolvers = {
730731
removeUserFromOrganizationGroups,
731732
bulkImportProjectsAndGroupsToOrganization,
732733
addOrUpdateEnvironmentService,
733-
deleteEnvironmentService
734+
deleteEnvironmentService,
735+
environmentIdling,
734736
},
735737
Subscription: {
736738
backupChanged: backupSubscriber,

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

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import * as R from 'ramda';
22
// @ts-ignore
33
import { sendToLagoonLogs } from '@lagoon/commons/dist/logs/lagoon-logger';
44
// @ts-ignore
5-
import { createRemoveTask } from '@lagoon/commons/dist/tasks';
5+
import {
6+
createRemoveTask,
7+
createMiscTask
8+
} from '@lagoon/commons/dist/tasks';
69
import { ResolverFn } from '../';
710
import { logger } from '../../loggers/logger';
811
import { isPatchEmpty, query, knex } from '../../util/db';
@@ -650,7 +653,8 @@ export const updateEnvironment: ResolverFn = async (
650653
route: input.patch.route,
651654
routes: input.patch.routes,
652655
autoIdle: input.patch.autoIdle,
653-
created: input.patch.created
656+
created: input.patch.created,
657+
idled: input.patch.idled
654658
}
655659
})
656660
);
@@ -675,7 +679,8 @@ export const updateEnvironment: ResolverFn = async (
675679
route: input.patch.route,
676680
routes: input.patch.routes,
677681
autoIdle: input.patch.autoIdle,
678-
created: input.patch.created
682+
created: input.patch.created,
683+
idled: input.patch.idled
679684
},
680685
data: withK8s
681686
}
@@ -930,4 +935,154 @@ export const getServiceContainersByServiceId: ResolverFn = async (
930935
Sql.selectContainersByServiceId(sid)
931936
);
932937
return await rows;
938+
}
939+
940+
export const environmentIdling = async (
941+
root,
942+
input,
943+
{ sqlClientPool, hasPermission, userActivityLogger }
944+
) => {
945+
const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id);
946+
947+
if (!environment) {
948+
throw new Error(
949+
'Invalid environment ID'
950+
);
951+
}
952+
953+
await hasPermission('environment', 'view', {
954+
project: environment.project
955+
});
956+
957+
// don't try idle if the environment is already idled or unidled
958+
if (environment.idled && input.idle) {
959+
throw new Error(
960+
`environment is already idled`
961+
);
962+
}
963+
if (!environment.idled && !input.idle) {
964+
throw new Error(
965+
`environment is already unidled`
966+
);
967+
}
968+
969+
const project = await projectHelpers(sqlClientPool).getProjectById(
970+
environment.project
971+
);
972+
973+
await hasPermission('deployment', 'cancel', {
974+
project: project.id
975+
});
976+
977+
const data = {
978+
environment,
979+
project,
980+
idling: {
981+
idle: input.idle,
982+
forceScale: input.disableAutomaticUnidling
983+
}
984+
};
985+
986+
userActivityLogger(`User requested environment idling for '${environment.name}'`, {
987+
project: '',
988+
event: 'api:idleEnvironment',
989+
payload: {
990+
project: project.name,
991+
environment: environment.name,
992+
idle: input.idle,
993+
disableAutomaticUnidling: input.disableAutomaticUnidling,
994+
}
995+
});
996+
997+
try {
998+
await createMiscTask({ key: 'environment:idling', data });
999+
return 'success';
1000+
} catch (error) {
1001+
sendToLagoonLogs(
1002+
'error',
1003+
'',
1004+
'',
1005+
'api:idleEnvironment',
1006+
{ environment: environment.id },
1007+
`Environment idle attempt possibly failed, reason: ${error}`
1008+
);
1009+
throw new Error(
1010+
error.message
1011+
);
1012+
}
1013+
};
1014+
1015+
export const environmentService = async (
1016+
root,
1017+
input,
1018+
{ sqlClientPool, hasPermission, userActivityLogger }
1019+
) => {
1020+
const environment = await Helpers(sqlClientPool).getEnvironmentById(input.id);
1021+
1022+
if (!environment) {
1023+
throw new Error(
1024+
'Invalid environment ID'
1025+
);
1026+
}
1027+
1028+
await hasPermission('environment', 'view', {
1029+
project: environment.project
1030+
});
1031+
1032+
// don't try idle if the environment is already idled or unidled
1033+
if (environment.idled && input.idle) {
1034+
throw new Error(
1035+
`environment is already idled`
1036+
);
1037+
}
1038+
if (!environment.idled && !input.idle) {
1039+
throw new Error(
1040+
`environment is already unidled`
1041+
);
1042+
}
1043+
1044+
const project = await projectHelpers(sqlClientPool).getProjectById(
1045+
environment.project
1046+
);
1047+
1048+
await hasPermission('deployment', 'cancel', {
1049+
project: project.id
1050+
});
1051+
1052+
const data = {
1053+
environment,
1054+
project,
1055+
lagoonService: {
1056+
name: input.serviceName,
1057+
state: input.state
1058+
}
1059+
};
1060+
1061+
userActivityLogger(`User requested environment idling for '${environment.name}'`, {
1062+
project: '',
1063+
event: 'api:idleEnvironment',
1064+
payload: {
1065+
project: project.name,
1066+
environment: environment.name,
1067+
idle: input.idle,
1068+
disableAutomaticUnidling: input.disableAutomaticUnidling,
1069+
}
1070+
});
1071+
1072+
try {
1073+
await createMiscTask({ key: 'environment:idling', data });
1074+
return 'success';
1075+
} catch (error) {
1076+
sendToLagoonLogs(
1077+
'error',
1078+
'',
1079+
'',
1080+
'api:idleEnvironment',
1081+
{ environment: environment.id },
1082+
`Environment idle attempt possibly failed, reason: ${error}`
1083+
);
1084+
throw new Error(
1085+
error.message
1086+
);
1087+
}
9331088
};

services/api/src/typeDefs.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,10 @@ const typeDefs = gql`
940940
kubernetes: Kubernetes
941941
kubernetesNamespacePattern: String
942942
workflows: [Workflow]
943+
"""
944+
Is the environment currently idled
945+
"""
946+
idled: Boolean
943947
}
944948
945949
type EnvironmentHitsMonth {
@@ -2532,6 +2536,7 @@ const typeDefs = gql`
25322536
bulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput, detachNotification: Boolean): ProjectGroupsToOrganization
25332537
addOrUpdateEnvironmentService(input: AddEnvironmentServiceInput!): EnvironmentService
25342538
deleteEnvironmentService(input: DeleteEnvironmentServiceInput!): String
2539+
environmentIdling(id: Int!, idle: Boolean!, disableAutomaticUnidling: Boolean): String
25352540
}
25362541
25372542
type Subscription {

0 commit comments

Comments
 (0)