diff --git a/core/src/plugins/kubernetes/namespace.ts b/core/src/plugins/kubernetes/namespace.ts index a0ca3e8d33..15fa4099d7 100644 --- a/core/src/plugins/kubernetes/namespace.ts +++ b/core/src/plugins/kubernetes/namespace.ts @@ -24,35 +24,54 @@ import { V1Namespace } from "@kubernetes/client-node" import { isSubset } from "../../util/is-subset" import chalk from "chalk" import { NamespaceStatus } from "../../types/plugin/base" +import { KubernetesServerResource } from "./types" const GARDEN_VERSION = getPackageVersion() -type CreateNamespaceStatus = "pending" | "created" -const created: { [name: string]: CreateNamespaceStatus } = {} + +const cache: { + [name: string]: { + status: "pending" | "created" + resource?: KubernetesServerResource + } +} = {} + +interface EnsureNamespaceResult { + remoteResource?: KubernetesServerResource + patched: boolean + created: boolean +} /** * Makes sure the given namespace exists and has the configured annotations and labels. * * Returns the namespace resource if it was created or updated, or null if nothing was done. */ -export async function ensureNamespace(api: KubeApi, namespace: NamespaceConfig, log: LogEntry) { - if (!created[namespace.name]) { - created[namespace.name] = "pending" +export async function ensureNamespace( + api: KubeApi, + namespace: NamespaceConfig, + log: LogEntry +): Promise { + const result: EnsureNamespaceResult = { patched: false, created: false } + + if (!cache[namespace.name] || namespaceNeedsUpdate(cache[namespace.name].resource!, namespace)) { + cache[namespace.name] = { status: "pending" } + + // Get the latest remote namespace list const namespacesStatus = await api.core.listNamespace() - let remoteNamespace: V1Namespace | undefined = undefined for (const n of namespacesStatus.items) { if (n.status.phase === "Active") { - created[n.metadata.name] = "created" + cache[n.metadata.name] = { status: "created", resource: n } } if (n.metadata.name === namespace.name) { - remoteNamespace = n + result.remoteResource = n } } - if (created[namespace.name] !== "created") { + if (cache[namespace.name].status !== "created") { log.verbose("Creating namespace " + namespace.name) try { - return api.core.createNamespace({ + result.remoteResource = await api.core.createNamespace({ apiVersion: "v1", kind: "Namespace", metadata: { @@ -65,42 +84,48 @@ export async function ensureNamespace(api: KubeApi, namespace: NamespaceConfig, labels: namespace.labels, }, }) + result.created = true } catch (error) { throw new KubernetesError( `Namespace ${namespace.name} doesn't exist and Garden was unable to create it. You may need to create it manually or ask an administrator to do so.`, { error } ) } - } else if ( - remoteNamespace && - (!isSubset(remoteNamespace.metadata?.annotations, namespace.annotations) || - !isSubset(remoteNamespace.metadata?.labels, namespace.labels)) - ) { + } else if (namespaceNeedsUpdate(result.remoteResource, namespace)) { // Make sure annotations and labels are set correctly if the namespace already exists log.verbose("Updating annotations and labels on namespace " + namespace.name) try { - return api.core.patchNamespace(namespace.name, { + result.remoteResource = await api.core.patchNamespace(namespace.name, { metadata: { annotations: namespace.annotations, labels: namespace.labels, }, }) + result.patched = true } catch { log.warn(chalk.yellow(`Unable to apply the configured annotations and labels on namespace ${namespace.name}`)) } } - created[namespace.name] = "created" + cache[namespace.name] = { status: "created", resource: result.remoteResource } } - return null + return result +} + +function namespaceNeedsUpdate(resource: KubernetesServerResource | undefined, config: NamespaceConfig) { + return ( + resource && + (!isSubset(resource.metadata?.annotations, config.annotations) || + !isSubset(resource.metadata?.labels, config.labels)) + ) } /** * Returns `true` if the namespace exists, `false` otherwise. */ export async function namespaceExists(api: KubeApi, name: string): Promise { - if (created[name]) { + if (cache[name]) { return true } diff --git a/core/test/integ/src/plugins/kubernetes/namespace.ts b/core/test/integ/src/plugins/kubernetes/namespace.ts index d2f88f2b8a..0587f37c29 100644 --- a/core/test/integ/src/plugins/kubernetes/namespace.ts +++ b/core/test/integ/src/plugins/kubernetes/namespace.ts @@ -49,13 +49,18 @@ describe("ensureNamespace", () => { const result = await ensureNamespace(api, namespace, log) - expect(result?.metadata.name).to.equal(namespaceName) - expect(result?.metadata.annotations).to.eql({ + const ns = result.remoteResource + + expect(ns?.metadata.name).to.equal(namespaceName) + expect(ns?.metadata.annotations).to.eql({ [gardenAnnotationKey("generated")]: "true", [gardenAnnotationKey("version")]: getPackageVersion(), ...namespace.annotations, }) - expect(result?.metadata.labels?.floo).to.equal("blar") + expect(ns?.metadata.labels?.floo).to.equal("blar") + + expect(result.created).to.be.true + expect(result.patched).to.be.false }) it("should add configured annotations if any are missing", async () => { @@ -78,12 +83,17 @@ describe("ensureNamespace", () => { const result = await ensureNamespace(api, namespace, log) - expect(result?.metadata.name).to.equal(namespaceName) - expect(result?.metadata.annotations).to.eql({ + const ns = result.remoteResource + + expect(ns?.metadata.name).to.equal(namespaceName) + expect(ns?.metadata.annotations).to.eql({ [gardenAnnotationKey("generated")]: "true", [gardenAnnotationKey("version")]: getPackageVersion(), foo: "bar", }) + + expect(result.created).to.be.false + expect(result.patched).to.be.true }) it("should add configured labels if any are missing", async () => { @@ -103,9 +113,14 @@ describe("ensureNamespace", () => { const result = await ensureNamespace(api, namespace, log) - expect(result?.metadata.name).to.equal(namespaceName) - expect(result?.metadata.labels?.foo).to.equal("bar") - expect(result?.metadata.labels?.floo).to.equal("blar") + const ns = result.remoteResource + + expect(ns?.metadata.name).to.equal(namespaceName) + expect(ns?.metadata.labels?.foo).to.equal("bar") + expect(ns?.metadata.labels?.floo).to.equal("blar") + + expect(result.created).to.be.false + expect(result.patched).to.be.true }) it("should do nothing if the namespace has already been configured", async () => { @@ -131,6 +146,7 @@ describe("ensureNamespace", () => { const result = await ensureNamespace(api, namespace, log) - expect(result).to.be.null + expect(result.created).to.be.false + expect(result.patched).to.be.false }) })