Skip to content

Commit ec6ec74

Browse files
committed
feat: make cleaner delete full namespace
Signed-off-by: osamamagdy <[email protected]>
1 parent 00151ad commit ec6ec74

File tree

10 files changed

+2132
-897
lines changed

10 files changed

+2132
-897
lines changed

cleaner/.jest/setEnvVars.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
process.env.MAX_INACTIVE_DURATION = '2d';
2+
process.env.SHOULD_DELETE = true;

cleaner/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
setupFiles: ['./.jest/setEnvVars.js'],
3+
// ... other configurations
4+
};

cleaner/package-lock.json

Lines changed: 1578 additions & 817 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cleaner/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
"author": "iteratec GmbH",
1111
"license": "Apache-2.0",
1212
"dependencies": {
13-
"@kubernetes/client-node": "^0.18.1"
13+
"@kubernetes/client-node": "^0.18.1",
14+
"jest-date-mock": "^1.0.8",
15+
"supertest": "^6.3.3",
16+
"winston": "^3.8.2"
1417
},
1518
"devDependencies": {
1619
"eslint": "^8.33.0",
1720
"eslint-plugin-prettier": "^4.2.1",
18-
"jest": "^29.4.2",
21+
"jest": "^29.4.3",
1922
"prettier": "^2.8.4"
2023
},
2124
"overrides": {

cleaner/src/kubernetes.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const { KubeConfig, AppsV1Api, CoreV1Api } = require('@kubernetes/client-node');
2+
const kc = new KubeConfig();
3+
kc.loadFromCluster();
4+
5+
const k8sAppsApi = kc.makeApiClient(AppsV1Api);
6+
const k8sCoreApi = kc.makeApiClient(CoreV1Api);
7+
8+
const { logger } = require('./logger');
9+
10+
const getTeamInstances = (namespaceName) =>
11+
k8sAppsApi.listNamespacedDeployment(namespaceName).catch((error) => {
12+
logger.info(error);
13+
throw new Error(error.response.body.message);
14+
});
15+
module.exports.getTeamInstances = getTeamInstances;
16+
17+
const getTeamJuiceShopInstances = (namespaceName) =>
18+
k8sAppsApi
19+
.listNamespacedDeployment(
20+
namespaceName,
21+
false,
22+
true,
23+
undefined,
24+
undefined,
25+
'app in (wrongsecrets, virtualdesktop)'
26+
)
27+
.catch((error) => {
28+
logger.info(error);
29+
throw new Error(error.response.body.message);
30+
});
31+
module.exports.getTeamJuiceShopInstances = getTeamJuiceShopInstances;
32+
33+
const getNamespaces = () =>
34+
k8sCoreApi.listNamespace(undefined, true, undefined, undefined, undefined, 200).catch((error) => {
35+
logger.info(error);
36+
throw new Error(error.response.body.message);
37+
});
38+
module.exports.getNamespaces = getNamespaces;
39+
40+
const deleteNamespaceForTeam = async (namespaceName) => {
41+
await k8sCoreApi.deleteNamespace(namespaceName).catch((error) => {
42+
throw new Error(error.response.body.message);
43+
});
44+
};
45+
module.exports.deleteNamespaceForTeam = deleteNamespaceForTeam;

cleaner/src/logger.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const winston = require('winston');
2+
3+
const myFormat = winston.format.printf(({ level, message, timestamp }) => {
4+
return `time="${timestamp}" level="${level}" msg="${message}"`;
5+
});
6+
7+
const getLogLevelForEnvironment = (env) => {
8+
switch (env) {
9+
case 'development':
10+
return 'debug';
11+
case 'test':
12+
return 'emerg';
13+
default:
14+
return 'info';
15+
}
16+
};
17+
18+
const logger = winston.createLogger({
19+
level: getLogLevelForEnvironment(process.env['NODE_ENV']),
20+
format: winston.format.combine(winston.format.timestamp(), myFormat),
21+
transports: [new winston.transports.Console()],
22+
});
23+
module.exports.logger = logger;

cleaner/src/main.js

Lines changed: 140 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
const { KubeConfig, AppsV1Api } = require('@kubernetes/client-node');
2-
31
const { parseTimeDurationString, msToHumanReadable } = require('./time');
42

5-
const kc = new KubeConfig();
6-
kc.loadFromCluster();
7-
8-
const k8sAppsApi = kc.makeApiClient(AppsV1Api);
3+
const {
4+
getTeamInstances,
5+
getTeamJuiceShopInstances,
6+
getNamespaces,
7+
deleteNamespaceForTeam,
8+
} = require('./kubernetes');
99

1010
const MaxInactiveDuration = process.env['MAX_INACTIVE_DURATION'];
11+
const ShouldDelete = process.env['SHOULD_DELETE'];
1112
const MaxInactiveDurationInMs = parseTimeDurationString(MaxInactiveDuration);
1213

1314
if (MaxInactiveDurationInMs === null) {
@@ -19,85 +20,148 @@ if (MaxInactiveDurationInMs === null) {
1920
"30m" for 30 minutes.
2021
`);
2122
}
22-
23+
/**
24+
* The approach is simple:
25+
* 1. Get all namespaces and loop over them
26+
* 2. For each namespace, get all wrongsecrets deployments and loop over them
27+
* 3. For each deployment, check if it has been inactive for more than the configured duration
28+
* 4. If it has, count it
29+
* 5. If it hasn't, skip it
30+
* 6. Repeat for all deployments
31+
* 7. If the number of inactive deployments equals the number of all deployments, delete the namespace
32+
* 8. Repeat for all namespaces
33+
* 9. Print out some stats
34+
* 10. Exit
35+
*/
2336
async function main() {
24-
const counts = {
25-
successful: {
26-
deployments: 0,
27-
services: 0,
28-
},
29-
failed: {
30-
deployments: 0,
31-
services: 0,
32-
},
33-
};
34-
35-
console.log(
36-
`Looking for Instances & namespaces which have been inactive for more than ${MaxInactiveDuration}.`
37-
);
38-
const instances = await k8sAppsApi.listDeploymentForAllNamespaces(
39-
true,
40-
undefined,
41-
undefined,
42-
'app in (wrongsecrets, virtualdesktop)',
43-
200
44-
);
45-
46-
console.log(`Found ${instances.body.items.length} instances. Checking their activity.`);
37+
console.log('Starting WrongSecrets Instance Cleanup');
38+
console.log('');
39+
console.log('Configuration:');
40+
console.log(` MAX_INACTIVE_DURATION: ${MaxInactiveDuration}`);
41+
console.log(` SHOULD_DELETE: ${ShouldDelete}`);
42+
console.log('');
43+
const namespacesNames = await listOldNamespaces();
44+
const counts = await deleteNamespaces(namespacesNames);
45+
console.log('');
46+
console.log('Finished WrongSecrets Instance Cleanup');
47+
console.log('');
48+
console.log('Successful deletions:');
49+
console.log(` Namespaces: ${counts.successful.namespaces}`);
50+
console.log('Failed deletions:');
51+
console.log(` Namespaces: ${counts.failed.namespaces}`);
52+
}
4753

48-
for (const instance of instances.body.items) {
49-
const instanceName = instance.metadata.name;
50-
const lastConnectTimestamps = parseInt(
51-
instance.metadata.annotations['wrongsecrets-ctf-party/lastRequest'],
52-
10
53-
);
54+
async function listOldNamespaces() {
55+
var namespacesNames = [];
56+
// Get all namespaces
57+
const namespaces = await getNamespaces();
58+
console.log(`Found ${namespaces.body.items.length} namespaces. Checking their activity.`);
59+
// Loop over all namespaces
60+
for (const namespace of namespaces.body.items) {
61+
console.log(`Checking ${namespace.metadata.name} namespaces activity.`);
62+
// Get the name of the namespace
63+
const namespaceName = namespace.metadata.name;
64+
console.log('Looking for deployments in namespace ' + namespaceName);
65+
// Get all deployments in the namespace
66+
const deployments = await getTeamJuiceShopInstances(namespaceName);
67+
// Check if deployments exist, if not, skip the namespace
68+
// IMPORTANT: In case the namespace is completely empty, it will not be deleted as the user might want it as a playground
69+
if (deployments === undefined || deployments.body.items.length === 0) {
70+
console.log(`No wrongsecrets deployments found in namespace ${namespaceName}. Skipping...`);
71+
continue;
72+
}
73+
// Check if the namespace is only used by the wrongsecrets instance. If not, skip the namespace
74+
const AllDeployments = await getTeamInstances(namespaceName);
75+
if (AllDeployments.body.items.length > deployments.body.items.length) {
76+
console.log(`Namespace ${namespaceName} is used by other deployments. Skipping...`);
77+
continue;
78+
}
79+
console.log(`Found ${deployments.body.items.length} wrongsecrets deployments
80+
in namespace ${namespaceName}.`);
81+
//Assume all deployments are active at first
82+
var numberOfActiveDeployments = deployments.body.items.length;
83+
// Loop over all deployments
84+
for (const deployment of deployments.body.items) {
85+
// Get the name of the deployment
86+
const deploymentName = deployment.metadata.name;
87+
const lastConnectTimestamps = parseInt(
88+
deployment.metadata.annotations['wrongsecrets-ctf-party/lastRequest'],
89+
10
90+
);
5491

55-
console.log(`Checking instance: '${instanceName}'.`);
92+
console.log(`Checking deployment: '${deploymentName}'.`);
5693

57-
const currentTime = new Date().getTime();
94+
const currentTime = new Date().getTime();
5895

59-
const timeDifference = currentTime - lastConnectTimestamps;
60-
var teamname = instance.metadata.labels.team;
61-
if (timeDifference > MaxInactiveDurationInMs) {
62-
console.log(
63-
`Instance: '${instanceName}'. Instance hasn't been used in ${msToHumanReadable(
64-
timeDifference
65-
)}.`
66-
);
67-
console.log(`Instance belongs to namespace ${teamname}`);
68-
try {
69-
console.log(`not yet implemented, but would be deleting namespace ${teamname} now`);
70-
// await k8sAppsApi.deleteNamespacedDeployment(instanceName, teamname);
71-
counts.successful.deployments++;
72-
} catch (error) {
73-
counts.failed.deployments++;
74-
console.error(`Failed to delete namespace '${teamname}'`);
75-
console.error(error);
96+
const timeDifference = currentTime - lastConnectTimestamps;
97+
var teamname = deployment.metadata.labels.team;
98+
if (timeDifference > MaxInactiveDurationInMs) {
99+
console.log(
100+
`Instance: '${deploymentName}'. Instance hasn't been used in ${msToHumanReadable(
101+
timeDifference
102+
)}.`
103+
);
104+
console.log(`Considered inactive.`);
105+
numberOfActiveDeployments--;
106+
} else {
107+
console.log(
108+
`Instance: '${deploymentName}' from '${teamname}'. Been last active ${msToHumanReadable(
109+
timeDifference
110+
)} ago.`
111+
);
112+
console.log(`Considered active. The namespace will not be deleted.`);
113+
// If the deployment is active, we can break the loop
114+
break;
76115
}
77-
} else {
78-
console.log(
79-
`Not deleting Instance: '${instanceName}' from '${teamname}'. Been last active ${msToHumanReadable(
80-
timeDifference
81-
)} ago.`
82-
);
116+
}
117+
// If all deployments are inactive, add the namespace to the list
118+
if (numberOfActiveDeployments === 0) {
119+
console.log(`All deployments in namespace ${namespaceName} are inactive. Should be deleted.`);
120+
namespacesNames.push(namespaceName);
83121
}
84122
}
123+
return namespacesNames;
124+
}
85125

126+
async function deleteNamespaces(namespaceNames) {
127+
const counts = {
128+
successful: {
129+
namespaces: 0,
130+
},
131+
failed: {
132+
namespaces: 0,
133+
},
134+
};
135+
// Check if the list is empty
136+
if (namespaceNames === undefined || namespaceNames.length === 0) {
137+
console.log('No namespaces to delete.');
138+
return counts;
139+
}
140+
// Check for the SHOULD_DELETE environment variable
141+
if (ShouldDelete === 'false') {
142+
console.log('SHOULD_DELETE is set to false. Skipping deletion.');
143+
return counts;
144+
}
145+
// Loop over all namespaces
146+
for (const namespaceName of namespaceNames) {
147+
console.log(`Deleting namespace ${namespaceName}...`);
148+
try {
149+
await deleteNamespaceForTeam(namespaceName);
150+
counts.successful.namespaces++;
151+
} catch (err) {
152+
counts.failed.namespaces++;
153+
console.error(`Failed to delete namespace ${namespaceName}.`);
154+
// console.error(err);
155+
}
156+
}
86157
return counts;
87158
}
159+
module.exports = {
160+
listOldNamespaces,
161+
deleteNamespaces,
162+
};
88163

89-
main()
90-
.then((counts) => {
91-
console.log('Finished WrongSecrets Instance Cleanup');
92-
console.log('');
93-
console.log('Successful deletions:');
94-
console.log(` Deployments: ${counts.successful.deployments}`);
95-
console.log(` Services: ${counts.successful.services}`);
96-
console.log('Failed deletions:');
97-
console.log(` Deployments: ${counts.failed.deployments}`);
98-
console.log(` Services: ${counts.failed.services}`);
99-
})
100-
.catch((err) => {
101-
console.error('Failed deletion tasks');
102-
console.error(err);
103-
});
164+
main().catch((err) => {
165+
console.error('Failed deletion tasks');
166+
console.error(err);
167+
});

0 commit comments

Comments
 (0)