Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'inject' command #33

Merged
merged 2 commits into from
Dec 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
})
})