Skip to content

Commit

Permalink
feat: Improve workspace:inject command (#639)
Browse files Browse the repository at this point in the history
* Improve workspace:inject command

Signed-off-by: Anatoliy Bazko <abazko@redhat.com>
  • Loading branch information
tolusha committed Apr 15, 2020
1 parent 1eedb6f commit 7a5ea3d
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 94 deletions.
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)_
Expand Down
18 changes: 16 additions & 2 deletions src/api/che.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
async getWorkspacePodName(namespace: string, cheWorkspaceId: string): Promise<string> {
const k8sApi = this.kc.makeApiClient(CoreV1Api)

const res = await k8sApi.listNamespacedPod(namespace)
Expand Down Expand Up @@ -437,10 +437,24 @@ export class CheHelper {
() => { })
}

async getAllWorkspaces(cheURL: string, accessToken?: string): Promise<any[]> {
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<any[]> {
const endpoint = `${cheUrl}/api/workspace?skipCount=${skipCount}&maxItems=${maxItems}`
const headers: any = { 'Content-Type': 'text/yaml' }
if (accessToken && accessToken.length > 0) {
Expand Down
113 changes: 63 additions & 50 deletions src/commands/workspace/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand All @@ -31,59 +33,78 @@ 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
private readonly command = getClusterClientCommand()

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)
}
Expand All @@ -94,7 +115,7 @@ export default class Inject extends Command {
})
}

async injectKubeconfigTasks(flags: any): Promise<Listr> {
async injectKubeconfig(flags: any, workspaceNamespace: string, workspacePodName: string, workspaceId: string): Promise<void> {
const kubeContext = flags['kube-context']
let contextToInject: Context | null
const kh = new KubeHelper(flags)
Expand All @@ -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<boolean> {
private async canInject(namespace: string, pod: string, container: string): Promise<boolean> {
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 }
}
Expand All @@ -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<void> {
private async doInjectKubeconfig(cheNamespace: string, workspacePod: string, container: string, contextToInject: Context): Promise<void> {
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('/')) {
Expand Down
12 changes: 1 addition & 11 deletions src/commands/workspace/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
}
})
Expand Down
2 changes: 1 addition & 1 deletion src/tasks/che.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}]
}
Expand Down
23 changes: 2 additions & 21 deletions test/api/che.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7a5ea3d

Please sign in to comment.