Skip to content

Commit c214036

Browse files
committed
feat: support idling and unidling from api with idled status on environment
1 parent 38ca596 commit c214036

File tree

10 files changed

+612
-429
lines changed

10 files changed

+612
-429
lines changed

node-packages/commons/src/tasks.ts

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

services/actions-handler/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ go 1.21
44

55
require (
66
github.com/cheshir/go-mq/v2 v2.0.1
7-
github.com/uselagoon/machinery v0.0.9
7+
github.com/uselagoon/machinery v0.0.15-0.20231215023103-5cdfc5838e87
88
gopkg.in/matryer/try.v1 v1.0.0-20150601225556-312d2599e12e
99
)
1010

1111
require (
1212
github.com/NeowayLabs/wabbit v0.0.0-20210927194032-73ad61d1620e // indirect
1313
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
14-
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
14+
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
1515
github.com/google/go-cmp v0.5.9 // indirect
1616
github.com/google/uuid v1.3.0 // indirect
1717
github.com/guregu/null v4.0.0+incompatible // indirect

services/actions-handler/go.sum

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
303303
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
304304
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
305305
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
306-
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
307306
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
308307
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
309308
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
@@ -407,6 +406,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
407406
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
408407
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
409408
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
409+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
410+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
410411
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
411412
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
412413
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -838,8 +839,8 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
838839
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
839840
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
840841
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
841-
github.com/uselagoon/machinery v0.0.9 h1:wj9CVSUtneh/ynt2by5UsaUoveFxkI7MY1c/EbR90p8=
842-
github.com/uselagoon/machinery v0.0.9/go.mod h1:IXLxlkahEAEgpCmu9Xa/Wmjo6ja4Aoq7tf8G7VrileE=
842+
github.com/uselagoon/machinery v0.0.15-0.20231215023103-5cdfc5838e87 h1:l6iDDMEy9pa0215arObnKpEXbaClAw7gtE8ACJRjfQ4=
843+
github.com/uselagoon/machinery v0.0.15-0.20231215023103-5cdfc5838e87/go.mod h1:h/qeMWQR4Qqu33x+8AulNDeolEwvb/G+aIsn/jyUtwk=
843844
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
844845
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
845846
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=

services/actions-handler/handler/controller_builds.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (m *Messenger) handleBuild(ctx context.Context, messageQueue *mq.MessageQue
3939

4040
// set up a lagoon client for use in the following process
4141
l := lclient.New(m.LagoonAPI.Endpoint, "actions-handler", &token, false)
42-
deployment, err := lagoon.GetDeploymentByName(ctx, message.Namespace, message.Meta.BuildName, l)
42+
deployment, err := lagoon.GetDeploymentByName(ctx, message.Meta.Project, message.Meta.Environment, message.Meta.BuildName, false, l)
4343
if err != nil {
4444
m.toLagoonLogs(messageQueue, map[string]interface{}{
4545
"severity": "error",
@@ -87,7 +87,7 @@ func (m *Messenger) handleBuild(ctx context.Context, messageQueue *mq.MessageQue
8787
"message": err.Error(),
8888
})
8989
if m.EnableDebug {
90-
log.Println(fmt.Sprintf("%sERROR: unable to get project - %v", prefix, err))
90+
log.Println(fmt.Sprintf("%sERROR: unable to get environment - %v", prefix, err))
9191
}
9292
return err
9393
}
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ const {
124124
userCanSshToEnvironment,
125125
getEnvironmentUrl,
126126
getEnvironmentsByKubernetes,
127+
idleEnvironment,
128+
unidleEnvironment,
127129
} = require('./resources/environment/resolvers');
128130

129131
const {
@@ -714,7 +716,9 @@ const resolvers = {
714716
removeDeployTargetFromOrganization,
715717
updateEnvironmentDeployTarget,
716718
removeUserFromOrganizationGroups,
717-
bulkImportProjectsAndGroupsToOrganization
719+
bulkImportProjectsAndGroupsToOrganization,
720+
idleEnvironment,
721+
unidleEnvironment
718722
},
719723
Subscription: {
720724
backupChanged: backupSubscriber,

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

Lines changed: 134 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';
@@ -668,7 +671,8 @@ export const updateEnvironment: ResolverFn = async (
668671
route: input.patch.route,
669672
routes: input.patch.routes,
670673
autoIdle: input.patch.autoIdle,
671-
created: input.patch.created
674+
created: input.patch.created,
675+
idled: input.patch.idled
672676
}
673677
})
674678
);
@@ -693,7 +697,8 @@ export const updateEnvironment: ResolverFn = async (
693697
route: input.patch.route,
694698
routes: input.patch.routes,
695699
autoIdle: input.patch.autoIdle,
696-
created: input.patch.created
700+
created: input.patch.created,
701+
idled: input.patch.idled
697702
},
698703
data: withK8s
699704
}
@@ -808,3 +813,129 @@ export const userCanSshToEnvironment: ResolverFn = async (
808813
return null;
809814
}
810815
};
816+
817+
export const idleEnvironment = async (
818+
root,
819+
args,
820+
{ sqlClientPool, hasPermission, userActivityLogger }
821+
) => {
822+
const environment = await Helpers(sqlClientPool).getEnvironmentById(args.id);
823+
824+
if (!environment) {
825+
throw new Error(
826+
'Invalid environment ID'
827+
);
828+
}
829+
830+
await hasPermission('environment', 'view', {
831+
project: environment.project
832+
});
833+
834+
if (!environment.idled) {
835+
const project = await projectHelpers(sqlClientPool).getProjectById(
836+
environment.project
837+
);
838+
839+
await hasPermission('deployment', 'cancel', {
840+
project: project.id
841+
});
842+
843+
const data = {
844+
environment,
845+
project,
846+
};
847+
848+
userActivityLogger(`User requested environment idling for '${environment.name}'`, {
849+
project: '',
850+
event: 'api:idleEnvironment',
851+
payload: {
852+
project: project.name,
853+
environment: environment.name
854+
}
855+
});
856+
857+
try {
858+
await createMiscTask({ key: 'idle:environment', data });
859+
return 'success';
860+
} catch (error) {
861+
sendToLagoonLogs(
862+
'error',
863+
'',
864+
'',
865+
'api:idleEnvironment',
866+
{ environment: environment.id },
867+
`Environment idle attempt possibly failed, reason: ${error}`
868+
);
869+
throw new Error(
870+
error.message
871+
);
872+
}
873+
} else {
874+
throw new Error(
875+
`environment is already idled`
876+
);
877+
}
878+
};
879+
880+
export const unidleEnvironment = async (
881+
root,
882+
args,
883+
{ sqlClientPool, hasPermission, userActivityLogger }
884+
) => {
885+
const environment = await Helpers(sqlClientPool).getEnvironmentById(args.id);
886+
887+
if (!environment) {
888+
throw new Error(
889+
'Invalid environment ID'
890+
);
891+
}
892+
893+
await hasPermission('environment', 'view', {
894+
project: environment.project
895+
});
896+
897+
if (environment.idled) {
898+
const project = await projectHelpers(sqlClientPool).getProjectById(
899+
environment.project
900+
);
901+
902+
await hasPermission('deployment', 'cancel', {
903+
project: project.id
904+
});
905+
906+
const data = {
907+
environment,
908+
project,
909+
};
910+
911+
userActivityLogger(`User requested environment unidle for '${environment.name}'`, {
912+
project: '',
913+
event: 'api:unidleEnvironment',
914+
payload: {
915+
project: project.name,
916+
environment: environment.name
917+
}
918+
});
919+
920+
try {
921+
await createMiscTask({ key: 'unidle:environment', data });
922+
return 'success';
923+
} catch (error) {
924+
sendToLagoonLogs(
925+
'error',
926+
'',
927+
'',
928+
'api:unidleEnvironment',
929+
{ environment: environment.id },
930+
`Environment unidle attempt possibly failed, reason: ${error}`
931+
);
932+
throw new Error(
933+
error.message
934+
);
935+
}
936+
} else {
937+
throw new Error(
938+
`environment is already unidled`
939+
);
940+
}
941+
};

services/api/src/typeDefs.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,10 @@ const typeDefs = gql`
928928
kubernetes: Kubernetes
929929
kubernetesNamespacePattern: String
930930
workflows: [Workflow]
931+
"""
932+
Is the environment currently idled
933+
"""
934+
idled: Boolean
931935
}
932936
933937
type EnvironmentHitsMonth {
@@ -2471,6 +2475,8 @@ const typeDefs = gql`
24712475
This mutation performs a lot of actions, on big project and group imports, if it times out, subsequent runs will perform only the changes necessary
24722476
"""
24732477
bulkImportProjectsAndGroupsToOrganization(input: AddProjectToOrganizationInput, detachNotification: Boolean): ProjectGroupsToOrganization
2478+
idleEnvironment(id: Int!): String
2479+
unidleEnvironment(id: Int!): String
24742480
}
24752481
24762482
type Subscription {

0 commit comments

Comments
 (0)