From f4c02b6bf2dcc204e76ece3c8d36abcc9fe11176 Mon Sep 17 00:00:00 2001 From: fmarek-kindred <123923685+fmarek-kindred@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:13:45 +1000 Subject: [PATCH] feat: Implement NS cleanup (#26) feat: Implement NS cleanup --- brownie/.gitignore | 1 + brownie/scripts/clean-ns.sh | 35 ++++++++++++++++++ brownie/src/k8ns/core.ts | 42 ++++++++++++++++++++++ brownie/src/k8ns/index.ts | 72 +++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100755 brownie/scripts/clean-ns.sh create mode 100644 brownie/src/k8ns/core.ts create mode 100644 brownie/src/k8ns/index.ts diff --git a/brownie/.gitignore b/brownie/.gitignore index 4ebc8ae..dc9a6da 100644 --- a/brownie/.gitignore +++ b/brownie/.gitignore @@ -1 +1,2 @@ coverage +brownie-ns-list.tmp.json diff --git a/brownie/scripts/clean-ns.sh b/brownie/scripts/clean-ns.sh new file mode 100755 index 0000000..14b9807 --- /dev/null +++ b/brownie/scripts/clean-ns.sh @@ -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 \ No newline at end of file diff --git a/brownie/src/k8ns/core.ts b/brownie/src/k8ns/core.ts new file mode 100644 index 0000000..49ea7a6 --- /dev/null +++ b/brownie/src/k8ns/core.ts @@ -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 + } +} + +export class Config { + constructor( + readonly dryRun: boolean, + readonly nsList: Array, + readonly retentionMinutes: number, + ) {} +} + +export const loadConfig = (params: Map): 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 = 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) + ) +} \ No newline at end of file diff --git a/brownie/src/k8ns/index.ts b/brownie/src/k8ns/index.ts new file mode 100644 index 0000000..598148b --- /dev/null +++ b/brownie/src/k8ns/index.ts @@ -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() + + 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) + }) \ No newline at end of file