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

feat: Improve workspace:inject command #639

Merged
merged 3 commits into from
Apr 15, 2020
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
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