Skip to content

Commit

Permalink
fix(k8s): uncaught error when trying to patch namespace resource
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald authored and thsig committed Oct 24, 2021
1 parent 6371af0 commit 2ba8f6d
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 28 deletions.
63 changes: 44 additions & 19 deletions core/src/plugins/kubernetes/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<V1Namespace>
}
} = {}

interface EnsureNamespaceResult {
remoteResource?: KubernetesServerResource<V1Namespace>
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<EnsureNamespaceResult> {
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: {
Expand All @@ -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<V1Namespace> | 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<boolean> {
if (created[name]) {
if (cache[name]) {
return true
}

Expand Down
34 changes: 25 additions & 9 deletions core/test/integ/src/plugins/kubernetes/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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
})
})

0 comments on commit 2ba8f6d

Please sign in to comment.