Skip to content

Commit

Permalink
Merge pull request #723 from garden-io/autofetch-kubectl
Browse files Browse the repository at this point in the history
feat(k8s): automatically fetch kubectl when needed
  • Loading branch information
edvald committed Apr 18, 2019
2 parents 772e0ca + d79f7a4 commit d692401
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 126 deletions.
9 changes: 5 additions & 4 deletions docs/basics/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ If you haven't already set up Homebrew, please follow [their installation instru

#### Step 2: Docker and local Kubernetes

To install Docker, Kubernetes and kubectl, we strongly recommend Docker for Mac.
To install Docker, Kubernetes and kubectl, we strongly recommend Docker for Mac. Garden itself doesn't require a local
installation of Kubernetes, but it is in most cases the preferred way of using it.

_Note: If you have an older version installed, you may need to update it in
order to enable Kubernetes support._
Expand Down Expand Up @@ -73,7 +74,7 @@ You need the following dependencies on your local machine to use Garden:
* [Docker](https://docs.docker.com/)
* Git
* rsync
* Local installation of Kubernetes and [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
* (in most cases) A local installation of Kubernetes and [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)

#### Step 1: Docker

Expand Down Expand Up @@ -135,8 +136,8 @@ environments:
```

If you happen to have installed both Minikube and a version of Docker for Mac with Kubernetes support enabled,
`garden` will choose whichever one is configured as the current context in your `kubectl` configuration, and if neither
is set as the current context, Docker for Mac is preferred by default.
`garden` will choose whichever one is configured as the current context in your `kubectl` configuration. If neither
is set as the current context, the first available context is used.

(If you're not yet familiar with Garden configuration files, see: [Configuration files](../using-garden/configuration-files.md))

Expand Down
31 changes: 22 additions & 9 deletions garden-service/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ export class ActionHelper implements TypeGuard {
// FIXME: We're calling getEnvironmentStatus before preparing the environment.
// Results in 404 errors for unprepared/missing services.
// See: https://github.com/garden-io/garden/issues/353
const statuses = await this.getEnvironmentStatus({ pluginName, log })

const entry = log.info({ section: "providers", msg: "Getting status...", status: "active" })
const statuses = await this.getEnvironmentStatus({ pluginName, log: entry })

const needUserInput = Object.entries(statuses)
.map(([name, status]) => ({ ...status, name }))
Expand All @@ -172,37 +174,48 @@ export class ActionHelper implements TypeGuard {
? `Plugin ${names} has been updated or hasn't been configured, and requires user input.`
: `Plugins ${names} have been updated or haven't been configured, and require user input.`

entry.setError()

throw new ConfigurationError(
`${msgPrefix}. Please run \`garden init\` and then re-run this command.`,
{ statuses },
)
}

const needPrep = Object.entries(handlers).filter(([name]) => {
const status = statuses[name] || { ready: false }
const needForce = status.detail && !!status.detail.needForce
const forcePrep = force || needForce
return forcePrep || !status.ready
})

const output = {}

if (needPrep.length > 0) {
entry.setState(`Preparing environment...`)
}

// sequentially go through the preparation steps, to allow plugins to request user input
for (const [name, handler] of Object.entries(handlers)) {
for (const [name, handler] of needPrep) {
const status = statuses[name] || { ready: false }
const needForce = status.detail && !!status.detail.needForce
const forcePrep = force || needForce

if (status.ready && !forcePrep) {
continue
}

const envLogEntry = log.info({
const envLogEntry = entry.info({
status: "active",
section: name,
msg: "Preparing environment...",
msg: "Configuring...",
})

await handler({ ...this.commonParams(handler, log), force: forcePrep, status, log: envLogEntry })

envLogEntry.setSuccess("Configured")
envLogEntry.setSuccess({ msg: chalk.green("Ready"), append: true })

output[name] = true
}

entry.setSuccess({ msg: chalk.green("Ready"), append: true })

return output
}

Expand Down
12 changes: 8 additions & 4 deletions garden-service/src/plugins/kubernetes/container/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ContainerModule, ContainerService } from "../../container/config"
import { createIngressResources } from "./ingress"
import { createServiceResources } from "./service"
import { waitForResources } from "../status"
import { applyMany, deleteObjectsByLabel } from "../kubectl"
import { apply, deleteObjectsByLabel } from "../kubectl"
import { getAppNamespace } from "../namespace"
import { PluginContext } from "../../../plugin-context"
import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../../constants"
Expand All @@ -36,16 +36,19 @@ export async function deployContainerService(params: DeployServiceParams<Contain
const k8sCtx = <KubernetesPluginContext>ctx

const namespace = await getAppNamespace(k8sCtx, k8sCtx.provider)
const objects = await createContainerObjects(k8sCtx, service, runtimeContext, hotReload)
const manifests = await createContainerObjects(k8sCtx, service, runtimeContext, hotReload)

// TODO: use Helm instead of kubectl apply
const context = k8sCtx.provider.config.context
const pruneSelector = "service=" + service.name
await applyMany(k8sCtx.provider.config.context, objects, { force, namespace, pruneSelector })

await apply({ log, context, manifests, force, namespace, pruneSelector })

await waitForResources({
ctx: k8sCtx,
provider: k8sCtx.provider,
serviceName: service.name,
resources: objects,
resources: manifests,
log,
})

Expand Down Expand Up @@ -384,6 +387,7 @@ export async function deleteService(params: DeleteServiceParams): Promise<Servic
const context = provider.config.context
await deleteContainerDeployment({ namespace, context, serviceName: service.name, log })
await deleteObjectsByLabel({
log,
context,
namespace,
labelKey: "service",
Expand Down
10 changes: 7 additions & 3 deletions garden-service/src/plugins/kubernetes/container/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { KubernetesPluginContext, KubernetesProvider } from "../kubernetes"
import { storeTaskResult } from "../task-results"

export async function execInService(params: ExecInServiceParams<ContainerModule>) {
const { ctx, service, command, interactive } = params
const { ctx, log, service, command, interactive } = params
const k8sCtx = <KubernetesPluginContext>ctx
const provider = k8sCtx.provider
const api = new KubeApi(provider.config.context)
Expand Down Expand Up @@ -68,8 +68,12 @@ export async function execInService(params: ExecInServiceParams<ContainerModule>
}

const kubecmd = ["exec", ...opts, pod.metadata.name, "--", ...command]
const res = await kubectl(api.context, namespace).call(kubecmd, {
ignoreError: true,
const res = await kubectl.spawnAndWait({
log,
context: api.context,
namespace,
args: kubecmd,
reject: false,
timeout: 999999,
tty: interactive,
})
Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export async function deployService(
containerName: resourceSpec && resourceSpec.containerName,
})

await apply(provider.config.context, hotReloadTarget, { namespace })
await apply({ log, context: provider.config.context, manifests: [hotReloadTarget], namespace })
}

// FIXME: we should get these objects from the cluster, and not from the local `helm template` command, because
Expand Down
6 changes: 3 additions & 3 deletions garden-service/src/plugins/kubernetes/helm/tiller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { KubeApi } from "../api"
import { getAppNamespace } from "../namespace"
import { checkResourceStatuses, waitForResources } from "../status"
import { combineStates } from "../../../types/service"
import { applyMany } from "../kubectl"
import { apply } from "../kubectl"
import { KubernetesProvider } from "../kubernetes"
import chalk from "chalk"

Expand Down Expand Up @@ -47,12 +47,12 @@ export async function installTiller(ctx: PluginContext, provider: KubernetesProv

// Need to install the RBAC stuff ahead of Tiller
const roleResources = getRoleResources(namespace)
await applyMany(context, roleResources, { namespace })
await apply({ log, context, manifests: roleResources, namespace })
await waitForResources({ ctx, provider, serviceName: "tiller", resources: roleResources, log })

const tillerResources = await getTillerResources(ctx, provider, log)
const pruneSelector = "app=helm,name=tiller"
await applyMany(context, tillerResources, { namespace, pruneSelector })
await apply({ log, context, manifests: tillerResources, namespace, pruneSelector })
await waitForResources({ ctx, provider, serviceName: "tiller", resources: tillerResources, log })

entry.setSuccess({ msg: chalk.green(`Done (took ${entry.getDuration(1)} sec)`), append: true })
Expand Down
8 changes: 6 additions & 2 deletions garden-service/src/plugins/kubernetes/hot-reload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,12 @@ async function getLocalRsyncPort(ctx: PluginContext, log: LogEntry, targetDeploy
log.debug(`Forwarding local port ${rsyncLocalPort} to ${targetDeployment} sync container port ${RSYNC_PORT}`)

// TODO: use the API directly instead of kubectl (need to reverse engineer kubectl a bit to get how that works)
const proc = kubectl(k8sCtx.provider.config.context, namespace)
.spawn(["port-forward", targetDeployment, portMapping])
const proc = await kubectl.spawn({
log,
context: k8sCtx.provider.config.context,
namespace,
args: ["port-forward", targetDeployment, portMapping],
})

return new Promise((resolve) => {
proc.on("error", (error) => {
Expand Down
24 changes: 10 additions & 14 deletions garden-service/src/plugins/kubernetes/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,22 @@ const SYSTEM_NAMESPACE_MIN_VERSION = "0.9.0"
/**
* Used by both the remote and local plugin
*/
async function prepareNamespaces({ ctx }: GetEnvironmentStatusParams) {
async function prepareNamespaces({ ctx, log }: GetEnvironmentStatusParams) {
const k8sCtx = <KubernetesPluginContext>ctx
const kubeContext = k8sCtx.provider.config.context

try {
// TODO: use API instead of kubectl (I just couldn't find which API call to make)
await kubectl(kubeContext).call(["version"])
await kubectl.exec({ log, context: kubeContext, args: ["version"] })
} catch (err) {
// TODO: catch error properly
if (err.detail.output) {
throw new DeploymentError(
`Unable to connect to Kubernetes cluster. ` +
`Please make sure it is running, reachable and that you have the right context configured.`,
{
kubeContext,
kubectlOutput: err.detail.output,
},
)
}
throw err
throw new DeploymentError(
`Unable to connect to Kubernetes cluster. ` +
`Please make sure it is running, reachable and that you have the right context configured.`,
{
kubeContext,
...err,
},
)
}

await Bluebird.all([
Expand Down
Loading

0 comments on commit d692401

Please sign in to comment.