Skip to content

Commit

Permalink
Add 'inject' command (#33)
Browse files Browse the repository at this point in the history
* Add 'inject' command

Signed-off-by: Artem Zatsarynnyi <azatsary@redhat.com>
  • Loading branch information
azatsarynnyy authored Dec 31, 2018
1 parent 9c8d8a0 commit cb49bac
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ USAGE
* [`chectl server:start`](#chectl-serverstart)
* [`chectl server:stop`](#chectl-serverstop)
* [`chectl server:update`](#chectl-serverupdate)
* [`chectl workspace:inject`](#chectl-workspaceinject)
* [`chectl workspace:list`](#chectl-workspacelist)
* [`chectl workspace:start`](#chectl-workspacestart)
* [`chectl workspace:stop`](#chectl-workspacestop)
Expand Down Expand Up @@ -140,6 +141,24 @@ OPTIONS

_See code: [src/commands/server/update.ts](https://github.com/che-incubator/chectl/blob/v0.0.2/src/commands/server/update.ts)_

## `chectl workspace:inject`

inject configurations and tokens in a Che Workspace

```
USAGE
$ chectl workspace:inject
OPTIONS
-c, --container=container [default: theia-ide] Target container
-h, --help show CLI help
-k, --kubeconfig Inject the local Kubernetes configuration
-n, --chenamespace=chenamespace [default: kube-che] Kubernetes namespace where Che workspace is deployed
-w, --workspace=workspace Target workspace
```

_See code: [src/commands/workspace/inject.ts](https://github.com/che-incubator/chectl/blob/v0.0.2/src/commands/workspace/inject.ts)_

## `chectl workspace:list`

list Che workspaces
Expand Down
133 changes: 133 additions & 0 deletions src/commands/workspace/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// tslint:disable:object-curly-spacing

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

import { KubeConfig } from '@kubernetes/client-node'
import { Command, flags } from '@oclif/command'
import { string } from '@oclif/parser/lib/flags'

import { CheHelper } from '../../helpers/che'

export default class Inject extends Command {
static description = 'inject configurations and tokens in a Che Workspace'

static flags = {
help: flags.help({ char: 'h' }),
kubeconfig: flags.boolean({
char: 'k',
description: 'Inject the local Kubernetes configuration'
}),
workspace: string({
char: 'w',
description: 'Target workspace'
}),
container: string({
char: 'c',
description: 'Target container',
default: 'dev'
}),
chenamespace: string({
char: 'n',
description: 'Kubernetes namespace where Che workspace is running',
default: 'kube-che',
env: 'CHE_NAMESPACE'
}),
}

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'})
}
}
},
{
title: 'Verify if the workspaces is running',
task: async (ctx: any) => {
ctx.pod = await che.getWorkspacePod(flags.chenamespace!, flags.workspace).catch(e => this.error(e.message))
}
},
{
title: `Verify if container ${flags.container} exists`,
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.`)
}
}
},
{
title: 'Injecting Kubernetes configuration',
skip: () => {
if (!flags.kubeconfig) {
return 'Currently, injecting only the local 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)) },
])

try {
await tasks.run()
} catch (err) {
this.error(err)
}

notifier.notify({
title: 'chectl',
message: `Command ${this.id} has completed.`
})
}

/**
* Copies the local kubeconfig (only minikube context) in a Che Workspace.
* Returns true if file is injected successfully and false otherwise.
*/
async injectKubeconfig(cheNamespace: string, workspacePod: string, container: string): Promise<boolean> {
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
}
await execa.shell(`kubectl exec ${workspacePod} -n ${cheNamespace} -c ${container} -- mkdir ${containerHomeDir}/.kube -p`, { timeout: 10000 })

const kc = new KubeConfig()
kc.loadFromDefault()
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`)
}
const kubeconfig = path.join(os.tmpdir(), 'che-kubeconfig')
const cluster = kc.getCluster(contextToInject.cluster)
const user = kc.getUser(contextToInject.user)
await execa('kubectl', ['config', '--kubeconfig', kubeconfig, 'set-cluster', cluster.name, `--server=${cluster.server}`, `--certificate-authority=${cluster.caFile}`, '--embed-certs=true'], { timeout: 10000 })
await execa('kubectl', ['config', '--kubeconfig', kubeconfig, 'set-credentials', user.name, `--client-certificate=${user.certFile}`, `--client-key=${user.keyFile}`, '--embed-certs=true'], { timeout: 10000 })
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
}

async fileExists(namespace: string, pod: string, container: string, file: string): Promise<boolean> {
const { code } = await execa.shell(`kubectl exec ${pod} -n ${namespace} -c ${container} -- test -e ${file}`, { timeout: 10000, reject: false })
if (code === 0) { return true } else { return false }
}

async containerExists(namespace: string, pod: string, container: string): Promise<boolean> {
const { stdout } = await execa('kubectl', ['get', 'pods', `${pod}`, '-n', `${namespace}`, '-o', 'jsonpath={.spec.containers[*].name}'], { timeout: 10000 })
return stdout.split(' ').some(c => c === container)
}
}
30 changes: 30 additions & 0 deletions src/helpers/che.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,36 @@ export class CheHelper {
return found
}

/**
* Finds a pod where Che workspace is running.
* Rejects if no workspace is found for the given workspace ID
* or if workspace ID wasn't specified but more than one workspace is found.
*/
async getWorkspacePod(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.metadata.name
}
throw new Error('Pod is not found for the given workspace ID')
} else {
if (wsPods.length === 1) {
return wsPods[0].metadata.name
}
throw new Error('More than one pod with running workspace is found. Please, specify Che Workspace ID.')
}
}

async cheURL(namespace: string | undefined = ''): Promise<string> {
const protocol = 'http'
const { stdout } = await execa('kubectl',
Expand Down
35 changes: 35 additions & 0 deletions test/helpers/che.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CheHelper } from '../../src/helpers/che'

const sinon = require('sinon')
const namespace = 'kube-che'
const workspace = 'workspace-0123'
const k8sURL = 'https://192.168.64.34:8443'
const cheURL = 'https://che-kube-che.192.168.64.34.nip.io'
let ch = new CheHelper()
Expand Down Expand Up @@ -146,4 +147,38 @@ describe('Che helper', () => {
// const res = await ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.valid')
// expect(res).to.equal('http://che-kube-che.192.168.64.39.nip.io/che/chectl')
// })
describe('getWorkspacePod', () => {
fancy
.stub(kc, 'makeApiClient', () => k8sApi)
.stub(k8sApi, 'listNamespacedPod', () => ({ response: '', body: { items: [{ metadata: {name: 'pod-name', labels: {'che.workspace_id': workspace}} }] } }))
.it('should return pod name where workspace with the given ID is running', async () => {
const pod = await ch.getWorkspacePod(namespace, workspace)
expect(pod).to.equal('pod-name')
})
fancy
.stub(kc, 'makeApiClient', () => k8sApi)
.stub(k8sApi, 'listNamespacedPod', () => ({ response: '', body: { items: [{ metadata: {name: 'pod-name', labels: {'che.workspace_id': workspace}} }] } }))
.it('should detect a pod where single workspace is running', async () => {
const pod = await ch.getWorkspacePod(namespace)
expect(pod).to.equal('pod-name')
})
fancy
.stub(kc, 'makeApiClient', () => k8sApi)
.stub(k8sApi, 'listNamespacedPod', () => ({ response: '', body: { items: [] } }))
.do(() => ch.getWorkspacePod(namespace))
.catch(/No workspace pod is found/)
.it('should fail if no workspace is running')
fancy
.stub(kc, 'makeApiClient', () => k8sApi)
.stub(k8sApi, 'listNamespacedPod', () => ({ response: '', body: { items: [{ metadata: {labels: {'che.workspace_id': `${workspace}1`}} }] } }))
.do(() => ch.getWorkspacePod(namespace, workspace))
.catch(/Pod is not found for the given workspace ID/)
.it('should fail if no workspace is found for the given ID')
fancy
.stub(kc, 'makeApiClient', () => k8sApi)
.stub(k8sApi, 'listNamespacedPod', () => ({ response: '', body: { items: [{ metadata: {labels: {'che.workspace_id': workspace}} }, { metadata: {labels: {'che.workspace_id': `${workspace}1`}} }] } }))
.do(() => ch.getWorkspacePod(namespace))
.catch(/More than one pod with running workspace is found. Please, specify Che Workspace ID./)
.it('should fail if no workspace ID was provided but several workspaces are found')
})
})

0 comments on commit cb49bac

Please sign in to comment.