diff --git a/README.md b/README.md index 55bb33bd2..bf5091a42 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ _See code: [src/commands/workspace/list.ts](/~https://github.com/che-incubator/che ## `chectl workspace:logs` -Collect workspace logs +Collect workspace(s) logs ``` USAGE @@ -432,12 +432,11 @@ OPTIONS -d, --directory=directory Directory to store logs into -h, --help show CLI help - -n, --chenamespace=chenamespace [default: che] Kubernetes namespace where Che server is supposed to be - deployed - - -w, --workspace=workspace Target workspace. Can be omitted if only one Workspace is running + -n, --namespace=namespace (required) The namespace where workspace is located. Can be found in + workspace configuration 'attributes.infrastructureNamespace' field. - --follow Follow workspace creation logs + -w, --workspace=workspace (required) Target workspace id. Can be found in workspace configuration 'id' + field. --listr-renderer=default|silent|verbose [default: default] Listr renderer ``` diff --git a/src/api/che.ts b/src/api/che.ts index 46875474b..6e129ce20 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -15,6 +15,7 @@ import * as commandExists from 'command-exists' import * as fs from 'fs-extra' import * as yaml from 'js-yaml' import * as path from 'path' +import { setInterval } from 'timers' import { OpenShiftHelper } from '../api/openshift' @@ -305,78 +306,69 @@ export class CheHelper { } /** - * Reads logs from all new pods that starting to run. - * It basically lists existed pods and starts following logs from a new ones. + * Finds workspace pods and reads logs from it. */ - async readAllNewPodLog(namespace: string, directory: string): Promise { - const processedPods = new Set((await this.kube.listNamespacedPod(namespace)).items.map(pod => pod.metadata!.name!)) - setInterval(async () => this.readPodLogBySelectorIgnoreProcessed(namespace, undefined, directory, processedPods, true), CheHelper.POLL_INTERVAL) - } + async readWorkspacePodLog(namespace: string, workspaceId: string, directory: string): Promise { + const podLabelSelector = `che.workspace_id=${workspaceId}` - /** - * Reads logs from pods that match a given selector. - */ - async readPodLogBySelector(namespace: string, selector: string | undefined, directory: string, follow: boolean): Promise { - const processedPods = new Set() - if (follow) { - setInterval(async () => this.readPodLogBySelectorIgnoreProcessed(namespace, selector, directory, processedPods, follow), CheHelper.POLL_INTERVAL) - } else { - await this.readPodLogBySelectorIgnoreProcessed(namespace, selector, directory, processedPods, follow) - } - } + let workspaceIsRun = false - /** - * Reads logs from pods that match a given selector with exception of already processed ones. - * Once log is read the pod is marked as processed. - */ - async readPodLogBySelectorIgnoreProcessed(namespace: string, selector: string | undefined, directory: string, processedPods: Set, follow: boolean): Promise { - const pods = await this.kube.listNamespacedPod(namespace, selector) + const pods = await this.kube.listNamespacedPod(namespace, undefined, podLabelSelector) + if (pods.items.length) { + workspaceIsRun = true + } for (const pod of pods.items) { - const podName = pod.metadata!.name! - - if (!processedPods.has(podName)) { - processedPods.add(podName) - await this.readPodLogByName(namespace, podName, directory, follow) + for (const containerStatus of pod.status!.containerStatuses!) { + workspaceIsRun = workspaceIsRun && !!containerStatus.state && !!containerStatus.state.running } } + + const follow = !workspaceIsRun + await this.readPodLog(namespace, podLabelSelector, directory, follow) + await this.readNamespaceEvents(namespace, directory, follow) + + return workspaceIsRun } /** - * Reads log from pod that matches a given name. + * Reads logs from pods that match a given selector. */ - async readPodLogByName(namespace: string, podName: string, directory: string, follow: boolean): Promise { - const processedContainers = new Set() + async readPodLog(namespace: string, podLabelSelector: string | undefined, directory: string, follow: boolean): Promise { + const processedContainers = new Map>() if (follow) { - setInterval(async () => this.readPodLogByNameIgnoreProcessed(namespace, podName, directory, processedContainers, follow), 100) + setInterval(async () => this.readContainerLogIgnoreProcessed(namespace, podLabelSelector, directory, processedContainers, follow), CheHelper.POLL_INTERVAL) } else { - await this.readPodLogByNameIgnoreProcessed(namespace, podName, directory, processedContainers, follow) + await this.readContainerLogIgnoreProcessed(namespace, podLabelSelector, directory, processedContainers, follow) } } /** - * Reads log from all containers in the pod with exception of already processed ones. - * Once log is read the container is marked as processed. + * Reads containers logs inside pod that match a given selector. */ - async readPodLogByNameIgnoreProcessed(namespace: string, podName: string, directory: string, processedContainers: Set, follow: boolean): Promise { - const pod = await this.kube.readNamespacedPod(podName, namespace) - if (!pod) { - return - } + async readContainerLogIgnoreProcessed(namespace: string, podLabelSelector: string | undefined, directory: string, processedContainers: Map>, follow: boolean): Promise { + const pods = await this.kube.listNamespacedPod(namespace, undefined, podLabelSelector) - if (!pod.status || !pod.status.containerStatuses) { - return - } + for (const pod of pods.items) { + const podName = pod.metadata!.name! + if (!processedContainers.has(podName)) { + processedContainers.set(podName, new Set()) + } - for (const container of pod.status.containerStatuses) { - if (!container.state || !container.state.running) { - continue + if (!pod.status || !pod.status.containerStatuses) { + return } - const containerName = container.name - if (!processedContainers.has(containerName)) { - processedContainers.add(containerName) - await this.readContainerLog(namespace, podName, containerName, directory, follow) + for (const containerStatus of pod.status.containerStatuses) { + if (!containerStatus.state || !containerStatus.state.running) { + continue + } + + const containerName = containerStatus.name + if (!processedContainers.get(podName)!.has(containerName)) { + processedContainers.get(podName)!.add(containerName) + await this.readContainerLog(namespace, podName, containerName, directory, follow) + } } } } @@ -388,9 +380,13 @@ export class CheHelper { const fileName = path.resolve(directory, namespace, 'events.txt') fs.ensureFileSync(fileName) - const cliTool = (commandExists.sync('kubectl') && 'kubectl') || (commandExists.sync('oc') && 'oc') - if (cliTool) { - cp.exec(`${cliTool} get events -n ${namespace} ${follow ? '--watch' : ''} >> ${fileName}`) + const cli = (commandExists.sync('kubectl') && 'kubectl') || (commandExists.sync('oc') && 'oc') + if (cli) { + const command = 'get events' + const namespaceParam = `-n ${namespace}` + const watchParam = follow && '--watch' || '' + + cp.exec(`${cli} ${command} ${namespaceParam} ${watchParam} >> ${fileName}`) } else { throw new Error('No events are collected. \'kubectl\' or \'oc\' is required to perform the task.') } @@ -402,6 +398,7 @@ export class CheHelper { private async readContainerLog(namespace: string, podName: string, containerName: string, directory: string, follow: boolean): Promise { const fileName = path.resolve(directory, namespace, podName, `${containerName}.log`) fs.ensureFileSync(fileName) + return this.kube.readNamespacedPodLog(podName, namespace, containerName, fileName, follow) } diff --git a/src/api/kube.ts b/src/api/kube.ts index 1ce35051f..6862ab53e 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1LabelSelector, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1ServiceAccount, V1ServiceList, V1Subject } from '@kubernetes/client-node' +import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1LabelSelector, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1ServiceAccount, V1ServiceList, V1Subject } from '@kubernetes/client-node' import { Context } from '@kubernetes/client-node/dist/config_types' import axios from 'axios' import { cli } from 'cli-ux' @@ -1258,10 +1258,26 @@ export class KubeHelper { throw new Error('ERR_LIST_PVCS') } - async listNamespacedPod(namespace: string, selector?: string): Promise { + async listNamespace(): Promise { const k8sApi = this.kc.makeApiClient(CoreV1Api) try { - const res = await k8sApi.listNamespacedPod(namespace, true, undefined, undefined, undefined, selector) + const res = await k8sApi.listNamespace() + if (res && res.body) { + return res.body + } else { + return { + items: [] + } + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async listNamespacedPod(namespace: string, fieldSelector?: string, labelSelector?: string): Promise { + const k8sApi = this.kc.makeApiClient(CoreV1Api) + try { + const res = await k8sApi.listNamespacedPod(namespace, true, undefined, undefined, fieldSelector, labelSelector) if (res && res.body) { return res.body } else { diff --git a/src/commands/server/logs.ts b/src/commands/server/logs.ts index e368dcc61..fdb7f11a4 100644 --- a/src/commands/server/logs.ts +++ b/src/commands/server/logs.ts @@ -45,7 +45,7 @@ export default class Logs extends Command { tasks.add(k8sTasks.testApiTasks(flags, this)) tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) tasks.add(cheTasks.serverLogsTasks(flags, false)) - tasks.add(cheTasks.namespaceEventsTask(flags, this, false)) + tasks.add(cheTasks.namespaceEventsTask(flags.chenamespace, this, false)) try { this.log(`Eclipse Che logs will be available in '${ctx.directory}'`) diff --git a/src/commands/server/start.ts b/src/commands/server/start.ts index 310054070..8cd94b1c3 100644 --- a/src/commands/server/start.ts +++ b/src/commands/server/start.ts @@ -238,7 +238,7 @@ export default class Start extends Command { const eventTasks = new Listr([{ title: 'Start following events', - task: () => new Listr(cheTasks.namespaceEventsTask(flags, this, true)) + task: () => new Listr(cheTasks.namespaceEventsTask(flags.chenamespace, this, true)) }], listrOptions) try { diff --git a/src/commands/workspace/logs.ts b/src/commands/workspace/logs.ts index bcf686d9f..767a9e79c 100644 --- a/src/commands/workspace/logs.ts +++ b/src/commands/workspace/logs.ts @@ -15,24 +15,25 @@ import * as notifier from 'node-notifier' import * as os from 'os' import * as path from 'path' -import { cheNamespace, listrRenderer } from '../../common-flags' +import { listrRenderer } from '../../common-flags' import { CheTasks } from '../../tasks/che' import { K8sTasks } from '../../tasks/platforms/k8s' export default class Logs extends Command { - static description = 'Collect workspace logs' + static description = 'Collect workspace(s) logs' static flags = { help: flags.help({ char: 'h' }), - chenamespace: cheNamespace, 'listr-renderer': listrRenderer, - follow: flags.boolean({ - description: 'Follow workspace creation logs', - default: false - }), workspace: string({ char: 'w', - description: 'Target workspace. Can be omitted if only one Workspace is running' + description: 'Target workspace id. Can be found in workspace configuration \'id\' field.', + required: true + }), + namespace: string({ + char: 'n', + description: 'The namespace where workspace is located. Can be found in workspace configuration \'attributes.infrastructureNamespace\' field.', + required: true }), directory: string({ char: 'd', @@ -50,21 +51,16 @@ export default class Logs extends Command { const tasks = new Listr([], { renderer: flags['listr-renderer'] as any }) tasks.add(k8sTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - if (!flags.follow) { - tasks.add(cheTasks.verifyWorkspaceRunTask(flags, this)) - } - tasks.add(cheTasks.workspaceLogsTasks(flags)) - tasks.add(cheTasks.namespaceEventsTask(flags, this, flags.follow)) + tasks.add(cheTasks.workspaceLogsTasks(flags.namespace, flags.workspace)) try { this.log(`Eclipse Che logs will be available in '${ctx.directory}'`) await tasks.run(ctx) - if (flags.follow) { - this.log('chectl is still running and keeps collecting logs.') - } else { - this.log('Command workspace:logs has completed successfully.') + if (!ctx['workspace-run']) { + this.log(`Workspace ${flags.workspace} probably hasn't been started yet.`) + this.log('The program will keep running and collecting logs...') + this.log('Terminate the program when all logs are gathered...') } } catch (error) { this.error(error) diff --git a/src/tasks/che.ts b/src/tasks/che.ts index 5385a97a2..7b599d86b 100644 --- a/src/tasks/che.ts +++ b/src/tasks/che.ts @@ -496,63 +496,59 @@ export class CheTasks { { title: `${follow ? 'Start following' : 'Read'} Che logs`, task: async (ctx: any, task: any) => { - await this.che.readPodLogBySelector(flags.chenamespace, this.cheSelector, ctx.directory, follow) + await this.che.readPodLog(flags.chenamespace, this.cheSelector, ctx.directory, follow) task.title = await `${task.title}...done` } }, { title: `${follow ? 'Start following' : 'Read'} Postgres logs`, task: async (ctx: any, task: any) => { - await this.che.readPodLogBySelector(flags.chenamespace, this.postgresSelector, ctx.directory, follow) + await this.che.readPodLog(flags.chenamespace, this.postgresSelector, ctx.directory, follow) task.title = await `${task.title}...done` } }, { title: `${follow ? 'Start following' : 'Read'} Keycloak logs`, task: async (ctx: any, task: any) => { - await this.che.readPodLogBySelector(flags.chenamespace, this.keycloakSelector, ctx.directory, follow) + await this.che.readPodLog(flags.chenamespace, this.keycloakSelector, ctx.directory, follow) task.title = await `${task.title}...done` } }, { title: `${follow ? 'Start following' : 'Read'} Plugin registry logs`, task: async (ctx: any, task: any) => { - await this.che.readPodLogBySelector(flags.chenamespace, this.pluginRegistrySelector, ctx.directory, follow) + await this.che.readPodLog(flags.chenamespace, this.pluginRegistrySelector, ctx.directory, follow) task.title = await `${task.title}...done` } }, { title: `${follow ? 'Start following' : 'Read'} Devfile registry logs`, task: async (ctx: any, task: any) => { - await this.che.readPodLogBySelector(flags.chenamespace, this.devfileRegistrySelector, ctx.directory, follow) + await this.che.readPodLog(flags.chenamespace, this.devfileRegistrySelector, ctx.directory, follow) task.title = await `${task.title}...done` } } ] } - workspaceLogsTasks(flags: any): ReadonlyArray { + workspaceLogsTasks(namespace: string, workspaceId: string): ReadonlyArray { return [ { - title: `${flags.follow ? 'Start following' : 'Read'} workspace logs`, + title: 'Read workspace logs', task: async (ctx: any, task: any) => { - if (flags.follow) { - await this.che.readAllNewPodLog(flags.chenamespace, ctx.directory) - } else { - await this.che.readPodLogByName(flags.chenamespace, ctx.pod, ctx.directory, false) - } - task.title = await `${task.title}...done` + ctx['workspace-run'] = await this.che.readWorkspacePodLog(namespace, workspaceId, ctx.directory) + task.title = `${task.title}...done` } } ] } - namespaceEventsTask(flags: any, command: Command, follow: boolean): ReadonlyArray { + namespaceEventsTask(namespace: string, command: Command, follow: boolean): ReadonlyArray { return [ { title: `${follow ? 'Start following' : 'Read'} namespace events`, task: async (ctx: any, task: any) => { - await this.che.readNamespaceEvents(flags.chenamespace, ctx.directory, follow).catch(e => command.error(e.message)) + await this.che.readNamespaceEvents(namespace, ctx.directory, follow).catch(e => command.error(e.message)) task.title = await `${task.title}...done` } } diff --git a/yarn.lock b/yarn.lock index 5c3714393..77f0bb9d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1677,11 +1677,11 @@ ecc-jsbn@~0.1.1: "eclipse-che-operator@git://github.com/eclipse/che-operator#master": version "0.0.0" - resolved "git://github.com/eclipse/che-operator#18758810310ee6b163cf59719dcffb9e5b33ea28" + resolved "git://github.com/eclipse/che-operator#9332233235843963488193b9f69985372f87d8e6" "eclipse-che@git://github.com/eclipse/che#master": version "0.0.0" - resolved "git://github.com/eclipse/che#5ced53ef23269513de294be9689d91c4117fce95" + resolved "git://github.com/eclipse/che#a5f510162c8e5cf7d886246278193c1289a28b1f" editorconfig@^0.15.0: version "0.15.3"