Skip to content

Commit

Permalink
Inject configuration files in all containers if it's not specified (#85)
Browse files Browse the repository at this point in the history
* Inject configuration files in all containers if it's not specified

Signed-off-by: Artem Zatsarynnyi <[email protected]>

* Update the command's description

Signed-off-by: Artem Zatsarynnyi <[email protected]>

* Handle errors more gracefully

* Filter che-machine-exec container
  • Loading branch information
azatsarynnyy authored Apr 19, 2019
1 parent 66ce104 commit d885b10
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 22 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,17 @@ USAGE
$ chectl workspace:inject
OPTIONS
-c, --container=container [default: dev] Target container
-c, --container=container Target container. If not specified, configuration files will be injected in all
containers of a Che Workspace pod
-h, --help show CLI help
-k, --kubeconfig Inject the local Kubernetes configuration
-n, --chenamespace=chenamespace [default: che] Kubernetes namespace where Che workspace is running
-w, --workspace=workspace Target workspace
-w, --workspace=workspace Target workspace. Can be omitted if only one Workspace is running
--listr-renderer=listr-renderer [default: default] Listr renderer. Can be 'default', 'silent' or 'verbose'
```

Expand Down
25 changes: 25 additions & 0 deletions src/api/che.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ export class CheHelper {
}
}

async getWorkspacePodContainers(namespace: string, cheWorkspaceId?: string): Promise<string[]> {
this.kc.loadFromDefault()
const k8sApi = this.kc.makeApiClient(Core_v1Api)

const res = await k8sApi.listNamespacedPod(namespace)
const pods = res.body.items
const wsPods = pods.filter(pod => pod.metadata.labels['che.workspace_id'])
if (wsPods.length === 0) {
throw new Error('No workspace pod is found')
}

if (cheWorkspaceId) {
const wsPod = wsPods.find(p => p.metadata.labels['che.workspace_id'] === cheWorkspaceId)
if (wsPod) {
return wsPod.spec.containers.map(c => c.name)
}
throw new Error('Pod is not found for the given workspace ID')
} else {
if (wsPods.length === 1) {
return wsPods[0].spec.containers.map(c => c.name)
}
throw new Error('More than one pod with running workspace is found. Please, specify Che Workspace ID.')
}
}

async cheURL(namespace = ''): Promise<string> {
const kube = new KubeHelper()
if (await kube.isOpenShift()) {
Expand Down
75 changes: 55 additions & 20 deletions src/commands/workspace/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// tslint:disable:object-curly-spacing

import * as execa from 'execa'
import * as Listr from 'listr'
import * as os from 'os'
import * as path from 'path'

Expand All @@ -30,12 +31,12 @@ export default class Inject extends Command {
}),
workspace: string({
char: 'w',
description: 'Target workspace'
description: 'Target workspace. Can be omitted if only one Workspace is running'
}),
container: string({
char: 'c',
description: 'Target container',
default: 'dev'
description: 'Target container. If not specified, configuration files will be injected in all containers of a Che Workspace pod',
required: false
}),
chenamespace: string({
char: 'n',
Expand All @@ -51,15 +52,14 @@ export default class Inject extends Command {

async run() {
const { flags } = this.parse(Inject)
const Listr = require('listr')
const notifier = require('node-notifier')
const che = new CheHelper()
const tasks = new Listr([
{
title: `Verify if namespace ${flags.chenamespace} exists`,
task: async () => {
if (!await che.cheNamespaceExist(flags.chenamespace)) {
this.error(`E_BAD_NS - Namespace does not exist.\nThe Kubernetes Namespace "${flags.chenamespace}" doesn't exist. The Kubernetes configuration cannot be injected.\nFix with: verify the namespace where Che workspace is running (kubectl get --all-namespaces deployment | grep workspace)`, {code: 'EBADNS'})
this.error(`E_BAD_NS - Namespace does not exist.\nThe Kubernetes Namespace "${flags.chenamespace}" doesn't exist. The configuration cannot be injected.\nFix with: verify the namespace where Che workspace is running (kubectl get --all-namespaces deployment | grep workspace)`, {code: 'EBADNS'})
}
}
},
Expand All @@ -71,25 +71,23 @@ export default class Inject extends Command {
},
{
title: `Verify if container ${flags.container} exists`,
enabled: () => flags.container !== undefined,
task: async (ctx: any) => {
if (!await this.containerExists(flags.chenamespace!, ctx.pod, flags.container!)) {
this.error(`The container "${flags.container}" doesn't exist. The Kubernetes configuration cannot be injected.`)
this.error(`The specified container "${flags.container}" doesn't exist. The configuration cannot be injected.`)
}
}
},
{
title: 'Injecting Kubernetes configuration',
title: 'Injecting configurations',
skip: () => {
if (!flags.kubeconfig) {
return 'Currently, injecting only the local kubeconfig is supported. Please, specify flag -k'
return 'Currently, only injecting a kubeconfig is supported. Please, specify flag -k'
}
},
task: (ctx: any, task: any) => this.injectKubeconfig(flags.chenamespace!, ctx.pod, flags.container!).then(result => {
if (!result) {
task.skip('kubeconfig already exists in the target container')
}
}).catch(e => this.error(e.message)) },
], {renderer: flags['listr-renderer'] as any})
task: () => this.injectKubeconfigTasks(flags.chenamespace!, flags.workspace!, flags.container)
},
], {renderer: flags['listr-renderer'] as any, collapse: false})

try {
await tasks.run()
Expand All @@ -103,16 +101,53 @@ export default class Inject extends Command {
})
}

async injectKubeconfigTasks(chenamespace: string, workspace: string, container?: string): Promise<Listr> {
const che = new CheHelper()
const tasks = new Listr({exitOnError: false, concurrent: true})
const containers = container ? [container] : await che.getWorkspacePodContainers(chenamespace!, workspace!)
for (const cont of containers) {
// che-machine-exec container is very limited for a security reason.
// We cannot copy file into it.
if (cont === 'che-machine-exec') {
continue
}
tasks.add({
title: `injecting kubeconfig into container ${cont}`,
task: async (ctx: any, task: any) => {
try {
if (await this.canInject(chenamespace, ctx.pod, cont)) {
await this.injectKubeconfig(chenamespace!, ctx.pod, cont)
task.title = `${task.title}...done.`
} else {
task.skip('the container doesn\'t support file injection')
}
} catch (error) {
task.skip(error.message)
}
}
})
}
return tasks
}

/**
* Tests whether a file can be injected into the specified container.
*/
async canInject(namespace: string, pod: string, container: string): Promise<boolean> {
const { code } = await execa.shell(`kubectl exec ${pod} -n ${namespace} -c ${container} -- tar --version `, { timeout: 10000, reject: false })
if (code === 0) { return true } else { return false }
}

/**
* Copies the local kubeconfig (only minikube context) in a Che Workspace.
* Returns true if file is injected successfully and false otherwise.
* Copies the local kubeconfig (only minikube context) into the specified container.
* If returns, it means injection was completed successfully. If throws an error, injection failed
*/
async injectKubeconfig(cheNamespace: string, workspacePod: string, container: string): Promise<boolean> {
async injectKubeconfig(cheNamespace: string, workspacePod: string, container: string): Promise<void> {
const { stdout } = await execa.shell(`kubectl exec ${workspacePod} -n ${cheNamespace} -c ${container} env | grep ^HOME=`, { timeout: 10000 })
const containerHomeDir = stdout.split('=')[1]

if (await this.fileExists(cheNamespace, workspacePod, container, `${containerHomeDir}/.kube/config`)) {
return false
throw new Error('kubeconfig already exists in the target container')
}
await execa.shell(`kubectl exec ${workspacePod} -n ${cheNamespace} -c ${container} -- mkdir ${containerHomeDir}/.kube -p`, { timeout: 10000 })

Expand All @@ -121,7 +156,7 @@ export default class Inject extends Command {
const contextName = 'minikube'
const contextToInject = kc.getContexts().find(c => c.name === contextName)
if (!contextToInject) {
throw new Error(`Context ${contextName} is not found in the local kubeconfig`)
throw new Error(`Context ${contextName} is not found in the source kubeconfig`)
}
const kubeconfig = path.join(os.tmpdir(), 'che-kubeconfig')
const cluster = kc.getCluster(contextToInject.cluster)
Expand All @@ -131,7 +166,7 @@ export default class Inject extends Command {
await execa('kubectl', ['config', '--kubeconfig', kubeconfig, 'set-context', contextToInject.name, `--cluster=${contextToInject.cluster}`, `--user=${contextToInject.user}`, `--namespace=${cheNamespace}`], { timeout: 10000 })
await execa('kubectl', ['config', '--kubeconfig', kubeconfig, 'use-context', contextToInject.name], { timeout: 10000 })
await execa('kubectl', ['cp', kubeconfig, `${cheNamespace}/${workspacePod}:${containerHomeDir}/.kube/config`, '-c', container], { timeout: 10000 })
return true
return
}

async fileExists(namespace: string, pod: string, container: string, file: string): Promise<boolean> {
Expand Down

0 comments on commit d885b10

Please sign in to comment.