diff --git a/README.md b/README.md index dc15320bd..93932cc0c 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/src/commands/workspace/inject.ts b/src/commands/workspace/inject.ts new file mode 100644 index 000000000..abf63dfd8 --- /dev/null +++ b/src/commands/workspace/inject.ts @@ -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 { + 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 { + 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 { + const { stdout } = await execa('kubectl', ['get', 'pods', `${pod}`, '-n', `${namespace}`, '-o', 'jsonpath={.spec.containers[*].name}'], { timeout: 10000 }) + return stdout.split(' ').some(c => c === container) + } +} diff --git a/src/helpers/che.ts b/src/helpers/che.ts index bafe30a7c..62c944692 100644 --- a/src/helpers/che.ts +++ b/src/helpers/che.ts @@ -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 { + 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 { const protocol = 'http' const { stdout } = await execa('kubectl', diff --git a/test/helpers/che.test.ts b/test/helpers/che.test.ts index fb7125220..09b43bdeb 100644 --- a/test/helpers/che.test.ts +++ b/test/helpers/che.test.ts @@ -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() @@ -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') + }) })