diff --git a/README.md b/README.md index 62f5b54cd..88dcc7edd 100644 --- a/README.md +++ b/README.md @@ -490,21 +490,24 @@ USAGE $ chectl workspace:inject OPTIONS - -c, --container=container Target container. If not specified, configuration files will be injected in - all containers of a workspace pod + -c, --container=container The container name. If not specified, configuration files will be injected in all + containers of the workspace pod - -h, --help show CLI help + -h, --help show CLI help - -k, --kubeconfig Inject the local Kubernetes configuration + -k, --kubeconfig (required) Inject the local Kubernetes configuration - -n, --chenamespace=chenamespace [default: che] Kubernetes namespace where Eclipse Che server is supposed to - be deployed + -n, --chenamespace=chenamespace [default: che] Kubernetes namespace where Eclipse Che server is supposed to be + deployed - -w, --workspace=workspace Target workspace. Can be omitted if only one workspace is running + -w, --workspace=workspace The workspace id to inject configuration into. It can be omitted if the only one + running workspace exists. + Use workspace:list command to get all workspaces and their + statuses. - --kube-context=kube-context Kubeconfig context to inject + --access-token=access-token Eclipse Che OIDC Access Token - --listr-renderer=default|silent|verbose [default: default] Listr renderer + --kube-context=kube-context Kubeconfig context to inject ``` _See code: [src/commands/workspace/inject.ts](https://github.com/che-incubator/chectl/blob/v0.0.2/src/commands/workspace/inject.ts)_ diff --git a/src/api/che.ts b/src/api/che.ts index 8c87119c3..f1b1908ce 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -49,7 +49,7 @@ export class CheHelper { * 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 { + async getWorkspacePodName(namespace: string, cheWorkspaceId: string): Promise { const k8sApi = this.kc.makeApiClient(CoreV1Api) const res = await k8sApi.listNamespacedPod(namespace) @@ -437,10 +437,24 @@ export class CheHelper { () => { }) } + async getAllWorkspaces(cheURL: string, accessToken?: string): Promise { + const all: any[] = [] + const maxItems = 30 + let skipCount = 0 + + do { + const workspaces = await this.doGetWorkspaces(cheURL, skipCount, maxItems, accessToken) + all.push(...workspaces) + skipCount += workspaces.length + } while (all.length === maxItems) + + return all + } + /** * Returns list of workspaces */ - async getWorkspaces(cheUrl: string, skipCount: number, maxItems: number, accessToken = ''): Promise<[any]> { + async doGetWorkspaces(cheUrl: string, skipCount: number, maxItems: number, accessToken = ''): Promise { const endpoint = `${cheUrl}/api/workspace?skipCount=${skipCount}&maxItems=${maxItems}` const headers: any = { 'Content-Type': 'text/yaml' } if (accessToken && accessToken.length > 0) { diff --git a/src/commands/workspace/inject.ts b/src/commands/workspace/inject.ts index 5dd30a22b..f8ed9539d 100644 --- a/src/commands/workspace/inject.ts +++ b/src/commands/workspace/inject.ts @@ -12,6 +12,7 @@ import { KubeConfig } from '@kubernetes/client-node' import { Context } from '@kubernetes/client-node/dist/config_types' import { Command, flags } from '@oclif/command' import { string } from '@oclif/parser/lib/flags' +import { cli } from 'cli-ux' import * as execa from 'execa' import * as fs from 'fs' import * as Listr from 'listr' @@ -20,8 +21,9 @@ import * as path from 'path' import { CheHelper } from '../../api/che' import { KubeHelper } from '../../api/kube' -import { cheNamespace, listrRenderer } from '../../common-flags' +import { accessToken, cheNamespace } from '../../common-flags' import { CheTasks } from '../../tasks/che' +import { ApiTasks } from '../../tasks/platforms/api' import { getClusterClientCommand, OPENSHIFT_CLI } from '../../util' export default class Inject extends Command { @@ -31,23 +33,25 @@ export default class Inject extends Command { help: flags.help({ char: 'h' }), kubeconfig: flags.boolean({ char: 'k', - description: 'Inject the local Kubernetes configuration' + description: 'Inject the local Kubernetes configuration', + required: true }), workspace: string({ char: 'w', - description: 'Target workspace. Can be omitted if only one workspace is running' + description: `The workspace id to inject configuration into. It can be omitted if the only one running workspace exists. + Use workspace:list command to get all workspaces and their statuses.` }), container: string({ char: 'c', - description: 'Target container. If not specified, configuration files will be injected in all containers of a workspace pod', + description: 'The container name. If not specified, configuration files will be injected in all containers of the workspace pod', required: false }), 'kube-context': string({ description: 'Kubeconfig context to inject', required: false }), - chenamespace: cheNamespace, - 'listr-renderer': listrRenderer + 'access-token': accessToken, + chenamespace: cheNamespace } // Holds cluster CLI tool name: kubectl or oc @@ -55,35 +59,52 @@ export default class Inject extends Command { async run() { const { flags } = this.parse(Inject) + const notifier = require('node-notifier') const cheTasks = new CheTasks(flags) + const apiTasks = new ApiTasks() + const cheHelper = new CheHelper(flags) - const tasks = new Listr([], { renderer: flags['listr-renderer'] as any }) + const tasks = new Listr([], { renderer: 'silent' }) + tasks.add(apiTasks.testApiTasks(flags, this)) tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.verifyWorkspaceRunTask(flags, this)) - tasks.add([ - { - 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 specified container "${flags.container}" doesn't exist. The configuration cannot be injected.`) - } - } - }, - { - title: 'Injecting configurations', - skip: () => { - if (!flags.kubeconfig) { - return 'Currently, only injecting a kubeconfig is supported. Please, specify flag -k' - } - }, - task: () => this.injectKubeconfigTasks(flags) - }, - ]) try { await tasks.run() + + let workspaceId = flags.workspace + let workspaceNamespace = '' + + const cheURL = await cheHelper.cheURL(flags.chenamespace) + if (!flags['access-token'] && await cheHelper.isAuthenticationEnabled(cheURL)) { + cli.error('Authentication is enabled but \'access-token\' is not provided.\nSee more details with the --help flag.') + } + + if (!workspaceId) { + const workspaces = await cheHelper.getAllWorkspaces(cheURL, flags['access-token']) + const runningWorkspaces = workspaces.filter(w => w.status === 'RUNNING') + if (runningWorkspaces.length === 1) { + workspaceId = runningWorkspaces[0].id + workspaceNamespace = runningWorkspaces[0].attributes.infrastructureNamespace + } else if (runningWorkspaces.length === 0) { + cli.error('There are no running workspaces. Please start workspace first.') + } else { + cli.error('There are more than 1 running workspaces. Please, specify the workspace id by providing \'--workspace\' flag.\nSee more details with the --help flag.') + } + } else { + const workspace = await cheHelper.getWorkspace(cheURL, workspaceId, flags['access-token']) + if (workspace.status !== 'RUNNING') { + cli.error(`Workspace '${workspaceId}' is not running. Please start workspace first.`) + } + workspaceNamespace = workspace.attributes.infrastructureNamespace + } + + const workspacePodName = await cheHelper.getWorkspacePodName(workspaceNamespace, workspaceId!) + if (flags.container && !await this.containerExists(workspaceNamespace, workspacePodName, flags.container)) { + cli.error(`The specified container '${flags.container}' doesn't exist. The configuration cannot be injected.`) + } + + await this.injectKubeconfig(flags, workspaceNamespace, workspacePodName, workspaceId!) } catch (err) { this.error(err) } @@ -94,7 +115,7 @@ export default class Inject extends Command { }) } - async injectKubeconfigTasks(flags: any): Promise { + async injectKubeconfig(flags: any, workspaceNamespace: string, workspacePodName: string, workspaceId: string): Promise { const kubeContext = flags['kube-context'] let contextToInject: Context | null const kh = new KubeHelper(flags) @@ -109,37 +130,29 @@ export default class Inject extends Command { } const che = new CheHelper(flags) - const tasks = new Listr({ exitOnError: false, concurrent: true }) - const containers = flags.container ? [flags.container] : await che.getWorkspacePodContainers(flags.chenamespace!, flags.workspace!) - for (const cont of containers) { + const containers = flags.container ? [flags.container] : await che.getWorkspacePodContainers(workspaceNamespace, workspaceId) + for (const container of containers) { // che-machine-exec container is very limited for a security reason. // We cannot copy file into it. - if (cont.startsWith('che-machine-exec')) { + if (container.startsWith('che-machine-exec') || container.startsWith('che-jwtproxy')) { continue } - tasks.add({ - title: `injecting kubeconfig into container ${cont}`, - task: async (ctx: any, task: any) => { - try { - if (await this.canInject(flags.chenamespace, ctx.pod, cont)) { - await this.injectKubeconfig(flags.chenamespace!, ctx.pod, cont, contextToInject!) - task.title = `${task.title}...done.` - } else { - task.skip('the container doesn\'t support file injection') - } - } catch (error) { - task.skip(error.message) - } + + try { + if (await this.canInject(workspaceNamespace, workspacePodName, container)) { + await this.doInjectKubeconfig(workspaceNamespace, workspacePodName, container, contextToInject!) + cli.info(`Configuration successfully injected into ${container} container`) } - }) + } catch (error) { + cli.warn(`Failed to injected configuration into ${container} container.\nError: ${error.message}`) + } } - return tasks } /** * Tests whether a file can be injected into the specified container. */ - async canInject(namespace: string, pod: string, container: string): Promise { + private async canInject(namespace: string, pod: string, container: string): Promise { const { exitCode } = await execa(`${this.command} exec ${pod} -n ${namespace} -c ${container} -- tar --version `, { timeout: 10000, reject: false, shell: true }) if (exitCode === 0) { return true } else { return false } } @@ -148,7 +161,7 @@ export default class Inject extends Command { * Copies the local kubeconfig 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, contextToInject: Context): Promise { + private async doInjectKubeconfig(cheNamespace: string, workspacePod: string, container: string, contextToInject: Context): Promise { const { stdout } = await execa(`${this.command} exec ${workspacePod} -n ${cheNamespace} -c ${container} env | grep ^HOME=`, { timeout: 10000, shell: true }) let containerHomeDir = stdout.split('=')[1] if (!containerHomeDir.endsWith('/')) { diff --git a/src/commands/workspace/list.ts b/src/commands/workspace/list.ts index 9ff5c0ffe..bead51780 100644 --- a/src/commands/workspace/list.ts +++ b/src/commands/workspace/list.ts @@ -43,17 +43,7 @@ export default class List extends Command { title: 'Get workspaces', task: async (ctx, task) => { const cheHelper = new CheHelper(flags) - - const maxItems = 30 - let skipCount = 0 - let workspaces: any[] = [] - - do { - workspaces = await cheHelper.getWorkspaces(ctx.cheURL, skipCount, maxItems, flags['access-token']) - ctx.workspaces.push(...workspaces) - skipCount += workspaces.length - } while (workspaces.length === maxItems) - + ctx.workspaces = await cheHelper.getAllWorkspaces(ctx.cheURL, flags['access-token']) task.title = `${task.title}... done` } }) diff --git a/src/tasks/che.ts b/src/tasks/che.ts index aa60f1b00..1d176665b 100644 --- a/src/tasks/che.ts +++ b/src/tasks/che.ts @@ -478,7 +478,7 @@ export class CheTasks { return [{ title: 'Verify if the workspaces is running', task: async (ctx: any) => { - ctx.pod = await this.che.getWorkspacePod(flags.chenamespace!, flags.workspace).catch(e => command.error(e.message)) + ctx.pod = await this.che.getWorkspacePodName(flags.chenamespace!, flags.workspace).catch(e => command.error(e.message)) } }] } diff --git a/test/api/che.test.ts b/test/api/che.test.ts index b98427114..741624c52 100644 --- a/test/api/che.test.ts +++ b/test/api/che.test.ts @@ -198,34 +198,15 @@ describe('Eclipse Che helper', () => { .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) + const pod = await ch.getWorkspacePodName(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)) + .do(() => ch.getWorkspacePodName(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 Workspace ID./) - .it('should fail if no workspace ID was provided but several workspaces are found') }) describe('isAuthenticationEnabled', () => { fancy