Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement NS cleanup #26

Merged
merged 3 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions brownie/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
coverage
brownie-ns-list.tmp.json
35 changes: 35 additions & 0 deletions brownie/scripts/clean-ns.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# This script is invoked from the periodic CI pipeline. It will fetch the list of children
# namespaces from the kubernetes and pass that list into the node app. The node app will determine
# whether the namespace is old and should be removed or it hasn't been aged yet and should be
# kept in the cluster. If the "--dry-run" parameter is "false" then old namespaces will be removed.
# The actual removal of namespace is delegeated to
# ../k8s-deployer/scripts/k8s-manage-namespace.sh
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
K8S_NAMESPACE=$1
BROWNIE_TIMEOUT=$2
DRY_RUN=$3

if [ "${DRY_RUN}" == "" ]; then DRY_RUN="true"; fi

usage="Example: $0 my-parent-namespace 3days"
if [ "${K8S_NAMESPACE}" == "" ];
then
echo "Missing 1st parameter namespace"
echo $usage
exit 1
fi

if [ "${BROWNIE_TIMEOUT}" == "" ];
then
echo "Missing 2nd parameter namespace retention period"
echo $usage
exit 1
fi

LOG_FILE="./brownie-ns-list.tmp.json"

kubectl get ns -l "${K8S_NAMESPACE}.tree.hnc.x-k8s.io/depth=1" -ojson | jq '.items' > $LOG_FILE

node dist/src/k8ns/index.js --dry-run $DRY_RUN --ns-file $LOG_FILE --retention-period $BROWNIE_TIMEOUT
42 changes: 42 additions & 0 deletions brownie/src/k8ns/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as fs from "fs"

import { Config as BrownieConfig } from "../config.js"
import { logger } from "../logger.js"

export const HNC_PARENT_ANNOTATION = "hnc.x-k8s.io/subnamespace-of"
export interface ChildNamespace {
metadata: {
creationTimestamp: string,
name: string,
annotations: Array<string>
}
}

export class Config {
constructor(
readonly dryRun: boolean,
readonly nsList: Array<ChildNamespace>,
readonly retentionMinutes: number,
) {}
}

export const loadConfig = (params: Map<string, string>): Config => {
const dryRunRaw = params.get(BrownieConfig.PARAM_DRY_RUN)
const nsDataFile = params.get("--ns-file")
const retentionRaw = params.get(BrownieConfig.PARAM_RETENTION_PERIOD)

const nsListRaw = fs.readFileSync(`${ nsDataFile }`).toString("utf-8")
let nsList: Array<ChildNamespace> = null
try {
nsList = JSON.parse(nsListRaw)
} catch (e) {
logger.warn("loadConfig(): Could not parse the list of namespaces:\n%s", nsListRaw)
throw new Error(`Unable to parse raw list of namespaces in '${ nsDataFile }'`, { cause: e })
}

return new Config(
"false" !== dryRunRaw.toLowerCase(),
nsList,
BrownieConfig.parseRetention(retentionRaw)
)
}
72 changes: 72 additions & 0 deletions brownie/src/k8ns/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as Shell from "node:child_process"

import { logger } from "../logger.js"
import * as Core from "./core.js"

const main = async () => {
const params = new Map<string, string>()

let deletedCount = 0
for (let i = 2; i < process.argv.length; i+=2) {
params.set(process.argv[i], process.argv[i + 1])
}
const config = Core.loadConfig(params)
if (config.nsList.length == 0) {
logger.debug("There are no namespaces to delete")
return
}

logger.info("Analysing the list of %s namespaces", config.nsList.length)

for (let ns of config.nsList) {
logger.info("")

if (!ns.metadata.creationTimestamp) {
logger.info("Namespace '%s' does not have 'creationTimestamp' information. Skipping...", ns.metadata.name)
continue
}

const nsName = ns.metadata.name
const ageInMinutes = (new Date().getTime() - Date.parse(ns.metadata.creationTimestamp)) / 60_000
logger.info("Namespace '%s' was created at: %s. It is %s minutes old", nsName, ns.metadata.creationTimestamp, ageInMinutes.toFixed(2))
if (ageInMinutes < config.retentionMinutes) {
logger.info("Namespace '%s' hasn't aged enough. Will be cleaned after %s minutes", nsName, (config.retentionMinutes - ageInMinutes).toFixed(2))
continue
}

const parentNsName = ns.metadata.annotations[Core.HNC_PARENT_ANNOTATION]
if (!parentNsName) {
logger.warn("The child namespace '%s', does not have '%s' annotation. It might be misconfigured, hence need to be dealt with manually. Skipping...", nsName, Core.HNC_PARENT_ANNOTATION)
continue
}

logger.info("Deleting namespace '%s' under '%s'", nsName, parentNsName)
if (config.dryRun) {

logger.info("Namespace '%s > %s' has NOT been deleted (dry run mode).", parentNsName, nsName)
deletedCount++

} else {

try {

const script = "../k8s-deployer/scripts/k8s-manage-namespace.sh"
const scriptParams = [ parentNsName, "delete", nsName, 120 ]
Shell.spawnSync(script, scriptParams, { stdio: [ 'inherit', 'inherit', 'inherit' ] })
deletedCount++

} catch (e) {
logger.error("Unable to delete namespace %s > %s. Error: %s", parentNsName, nsName, e.message)
if (e.cause) logger.error(e.cause)
if (e.stack) logger.error("Stack:\n%s", e.stack)
}
}
}
}

main()
.catch(e => {
logger.error("Message: %s", e.message)
if (e.cause) logger.error(e.cause)
if (e.stack) logger.error("Stack:\n%s", e.stack)
})
Loading