Skip to content

Commit

Permalink
fix(k8s): play nice with Helm 2 (Tiller) when users still need it
Browse files Browse the repository at this point in the history
Users may need Tiller for non-Garden related reasons. We now no longer
automatically remove Tiller from project namespaces (but we do for
garden-system), and instead provide a command to do the cleanup and a
prompt for the user to run it.

NOTE: We need to update the 0.11 release notes to reflect this!
  • Loading branch information
edvald authored and eysi09 committed Jan 13, 2020
1 parent b244c8e commit 8f803c3
Show file tree
Hide file tree
Showing 15 changed files with 263 additions and 56 deletions.
3 changes: 3 additions & 0 deletions garden-service/src/config/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { deline, dedent } from "../util/string"

export type Primitive = string | number | boolean | null

export interface StringMap {
[key: string]: string
}
export interface PrimitiveMap {
[key: string]: Primitive
}
Expand Down
88 changes: 73 additions & 15 deletions garden-service/src/plugins/kubernetes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { LogEntry } from "../../logger/log-entry"
import { kubectl } from "./kubectl"
import { urlJoin } from "../../util/string"
import { KubernetesProvider } from "./config"
import { StringMap } from "../../config/common"

interface ApiGroupMap {
[groupVersion: string]: V1APIGroup
Expand Down Expand Up @@ -244,7 +245,7 @@ export class KubeApi {
}

log.debug(`Kubernetes: Getting API resource info for group ${apiVersion}`)
const res = await this.request(log, getGroupBasePath(apiVersion))
const res = await this.request({ log, path: getGroupBasePath(apiVersion) })

// We're only interested in the entities themselves, not the sub-resources
const resources = res.body.resources.filter((r: any) => !r.name.includes("/"))
Expand All @@ -264,7 +265,15 @@ export class KubeApi {
return resource
}

async request(log: LogEntry, path: string, opts: Omit<request.OptionsWithUrl, "url"> = {}): Promise<any> {
async request({
log,
path,
opts = {},
}: {
log: LogEntry
path: string
opts?: Omit<request.OptionsWithUrl, "url">
}): Promise<any> {
const baseUrl = this.config.getCurrentCluster()!.server
const url = urlJoin(baseUrl, path)

Expand All @@ -288,46 +297,95 @@ export class KubeApi {
}
}

async readBySpec(namespace: string, manifest: KubernetesResource, log: LogEntry) {
async readBySpec({ log, namespace, manifest }: { log: LogEntry; namespace: string; manifest: KubernetesResource }) {
log.silly(`Fetching Kubernetes resource ${manifest.apiVersion}/${manifest.kind}/${manifest.metadata.name}`)

const apiPath = await this.getApiPath(namespace, manifest, log)
const apiPath = await this.getApiPath({ manifest, log, namespace })

const res = await this.request({ log, path: apiPath })
return res.body
}

async replace({
log,
resource,
namespace,
}: {
log: LogEntry
resource: KubernetesServerResource
namespace?: string
}) {
log.silly(`Replacing Kubernetes resource ${resource.apiVersion}/${resource.kind}/${resource.metadata.name}`)

const apiPath = await this.getApiPath({ manifest: resource, log, namespace })

const res = await this.request(log, apiPath)
const res = await this.request({ log, path: apiPath, opts: { method: "put", body: resource } })
return res.body
}

async deleteBySpec(namespace: string, manifest: KubernetesResource, log: LogEntry) {
/**
* Applies the specified `annotations` to the given resource and persists to the cluster.
* Assumes the resource already exists in the cluster.
*/
async annotateResource({
log,
resource,
annotations,
}: {
log: LogEntry
resource: KubernetesServerResource
annotations: StringMap
}) {
// TODO: use patch instead of replacing (it's weirdly complex, unfortunately)
resource.metadata.annotations = { ...resource.metadata.annotations, ...annotations }
await this.replace({ log, resource })
return resource
}

async deleteBySpec({ namespace, manifest, log }: { namespace: string; manifest: KubernetesResource; log: LogEntry }) {
log.silly(`Deleting Kubernetes resource ${manifest.apiVersion}/${manifest.kind}/${manifest.metadata.name}`)

const apiPath = await this.getApiPath(namespace, manifest, log)
const apiPath = await this.getApiPath({ manifest, log, namespace })

try {
await this.request(log, apiPath, { method: "delete" })
await this.request({ log, path: apiPath, opts: { method: "delete" } })
} catch (err) {
if (err.code !== 404) {
throw err
}
}
}

private async getApiPath(namespace: string, manifest: KubernetesResource, log: LogEntry) {
private async getApiPath({
manifest,
log,
namespace,
}: {
manifest: KubernetesResource
log: LogEntry
namespace?: string
}) {
const resourceInfo = await this.getApiResourceInfo(log, manifest)
const apiVersion = manifest.apiVersion
const name = manifest.metadata.name
const basePath = getGroupBasePath(apiVersion)

return resourceInfo.namespaced
? `${basePath}/namespaces/${namespace}/${resourceInfo.name}/${name}`
? `${basePath}/namespaces/${namespace || manifest.metadata.namespace}/${resourceInfo.name}/${name}`
: `${basePath}/${resourceInfo.name}/${name}`
}

async upsert<K extends keyof CrudMap, O extends KubernetesResource<CrudMapTypes[K]>>(
kind: K,
namespace: string,
obj: O,
async upsert<K extends keyof CrudMap, O extends KubernetesResource<CrudMapTypes[K]>>({
kind,
namespace,
obj,
log,
}: {
kind: K
namespace: string
obj: O
log: LogEntry
) {
}) {
const api = this[crudMap[kind].group]
const name = obj.metadata.name

Expand Down
35 changes: 35 additions & 0 deletions garden-service/src/plugins/kubernetes/commands/remove-tiller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { PluginCommand } from "../../../types/plugin/command"
import chalk from "chalk"
import { KubernetesPluginContext } from "../config"
import { getAppNamespace } from "../namespace"
import { KubeApi } from "../api"
import { removeTiller } from "../helm/tiller"

export const removeTillerCmd: PluginCommand = {
name: "remove-tiller",
description: "Remove Tiller from project namespace.",

title: () => {
return `Removing Tiller from project namespace`
},

handler: async ({ ctx, log }) => {
const k8sCtx = <KubernetesPluginContext>ctx
const api = await KubeApi.factory(log, k8sCtx.provider)
const namespace = await getAppNamespace(ctx, log, k8sCtx.provider)

await removeTiller(k8sCtx, api, namespace, log)

log.info(chalk.green("\nDone!"))

return { result: {} }
},
}
32 changes: 25 additions & 7 deletions garden-service/src/plugins/kubernetes/helm/tiller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { writeFile } from "fs-extra"
import { resolve } from "path"
import { getValueArgs, getChartPath, getReleaseName } from "./common"
import { Garden } from "../../../garden"
import chalk from "chalk"
import { gardenAnnotationKey } from "../../../util/string"

// DEPRECATED: remove all this in v0.12.0

Expand All @@ -41,15 +43,18 @@ export async function migrateToHelm3({
namespace,
log,
sysGarden,
cleanup,
}: {
ctx: KubernetesPluginContext
api: KubeApi
namespace: string
log: LogEntry
sysGarden?: Garden
cleanup: boolean
}) {
const migrationLog = log.info(`-> Migrating from Helm 2.x (Tiller) to Helm 3 in namespace ${namespace}`)
let res

let res: any
// List all releases in Helm 2 (Tiller)
try {
res = await helm({
Expand Down Expand Up @@ -89,6 +94,7 @@ export async function migrateToHelm3({
(r: any) => r.Name === "garden-nginx" && r.Status === "DEPLOYED"
)
if (nginxDeployed && sysGarden) {
log.info(chalk.gray(`-> Migrating release ${namespace}/garden-nginx from Tiller to Helm 3`))
log.debug("Using Helm 2 to upgrade the garden-nginx release")
const actionRouter = await sysGarden.getActionRouter()
const dg = await sysGarden.getConfigGraph(log)
Expand Down Expand Up @@ -222,17 +228,29 @@ export async function migrateToHelm3({
}
}

// Remove Tiller resources
log.info(`-> Removing Tiller from namespace ${namespace}`)
await removeTiller(ctx, api, namespace, log)
// Mark namespace as migrated
const ns = await api.core.readNamespace(namespace)
const annotations = { [gardenAnnotationKey("helm-migrated")]: "true" }
await api.annotateResource({ log, resource: ns, annotations })

if (cleanup) {
log.info(`-> Removing Tiller from namespace ${namespace}`)
await removeTiller(ctx, api, namespace, log)

log.info(`-> Helm 3 migration complete!`)
log.info(`-> Helm 3 migration complete!`)
} else {
const removeTillerCmd = chalk.yellow.bold.underline(
`garden plugins ${ctx.provider.name} remove-tiller --env ${ctx.environmentName}`
)
log.info(`-> Helm 3 migration complete!`)
log.info(chalk.yellow(`-> Please run ${removeTillerCmd} to remove Tiller and related resources.`))
}
}

async function removeTiller(ctx: KubernetesPluginContext, api: KubeApi, namespace: string, log: LogEntry) {
export async function removeTiller(ctx: KubernetesPluginContext, api: KubeApi, namespace: string, log: LogEntry) {
const manifests = await getTillerManifests(ctx, log, namespace)

return Bluebird.map(manifests, (resource) => api.deleteBySpec(namespace, resource, log))
return Bluebird.map(manifests, (resource) => api.deleteBySpec({ namespace, manifest: resource, log }))
}

async function getTillerManifests(
Expand Down
Loading

0 comments on commit 8f803c3

Please sign in to comment.