From ad6ddd0d0fca36271bd55e14a9032adfecd52a1e Mon Sep 17 00:00:00 2001 From: Mykola Morhun Date: Wed, 16 Sep 2020 17:21:10 +0300 Subject: [PATCH] feat: Rework workspace commands to be able to work with user namespace permissions only (#849) * Rework workspace commands to be able to work with user namespace permissions only Signed-off-by: Mykola Morhun --- README.md | 33 ++- src/api/che-api-client.ts | 345 +++++++++++++++++++++++++++++++ src/api/che.ts | 283 +++---------------------- src/api/kube.ts | 27 ++- src/commands/workspace/create.ts | 77 ++++--- src/commands/workspace/delete.ts | 97 ++++----- src/commands/workspace/inject.ts | 91 ++++---- src/commands/workspace/list.ts | 49 ++--- src/commands/workspace/logs.ts | 22 +- src/commands/workspace/start.ts | 51 ++--- src/commands/workspace/stop.ts | 44 ++-- src/common-flags.ts | 8 + src/tasks/che.ts | 12 -- src/tasks/workspace-tasks.ts | 85 -------- src/util.ts | 5 + test/api/che-api-client.test.ts | 113 ++++++++++ test/api/che.test.ts | 99 +-------- test/e2e/util/e2e.ts | 51 ++--- 18 files changed, 779 insertions(+), 713 deletions(-) create mode 100644 src/api/che-api-client.ts delete mode 100644 src/tasks/workspace-tasks.ts create mode 100644 test/api/che-api-client.test.ts diff --git a/README.md b/README.md index c893d908e..899d21b44 100644 --- a/README.md +++ b/README.md @@ -571,6 +571,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --name=name Workspace name: overrides the workspace name to use instead of the one defined in the devfile. @@ -605,6 +608,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --delete-namespace Indicates that a Kubernetes namespace where workspace was created will be deleted as well @@ -646,6 +652,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --kube-context=kube-context Kubeconfig context to inject @@ -677,6 +686,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --skip-kubernetes-health-check Skip Kubernetes health check ``` @@ -692,18 +704,15 @@ USAGE $ chectl workspace:logs OPTIONS - -d, --directory=directory Directory to store logs into - -h, --help show CLI help + -d, --directory=directory Directory to store logs into + -h, --help show CLI help - -n, --namespace=namespace (required) The namespace where workspace is located. Can be found in - workspace configuration 'attributes.infrastructureNamespace' field. + -n, --namespace=namespace (required) The namespace where workspace is located. Can be found in workspace + configuration 'attributes.infrastructureNamespace' field. - -w, --workspace=workspace (required) Target workspace id. Can be found in workspace configuration 'id' - field. + -w, --workspace=workspace (required) Target workspace id. Can be found in workspace configuration 'id' field. - --listr-renderer=default|silent|verbose [default: default] Listr renderer - - --skip-kubernetes-health-check Skip Kubernetes health check + --skip-kubernetes-health-check Skip Kubernetes health check ``` _See code: [src/commands/workspace/logs.ts](https://github.com/che-incubator/chectl/blob/v0.0.2/src/commands/workspace/logs.ts)_ @@ -736,6 +745,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --skip-kubernetes-health-check Skip Kubernetes health check ``` @@ -767,6 +779,9 @@ OPTIONS https://www.eclipse.org/che/docs/che-7/administration-guide/authenticating-users/#obtaining-the-token-from-openshift -token-through-keycloak_authenticating-to-the-che-server. + --che-api-endpoint=che-api-endpoint + Eclipse Che server API endpoint + --skip-kubernetes-health-check Skip Kubernetes health check ``` diff --git a/src/api/che-api-client.ts b/src/api/che-api-client.ts new file mode 100644 index 000000000..907ec703a --- /dev/null +++ b/src/api/che-api-client.ts @@ -0,0 +1,345 @@ +/********************************************************************* + * Copyright (c) 2019-2020 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { che as chetypes } from '@eclipse-che/api' +import axios, { AxiosInstance } from 'axios' +import { cli } from 'cli-ux' +import * as https from 'https' + +import { sleep } from '../util' + +/** + * Singleton responsible for calls to Che API. + */ +let instance: CheApiClient | undefined +export class CheApiClient { + public defaultCheResponseTimeoutMs = 3000 + public readonly cheApiEndpoint: string + + private readonly axios: AxiosInstance + + private constructor(cheApiEndpoint: string) { + this.cheApiEndpoint = cheApiEndpoint + + // Make axios ignore untrusted certificate error for self-signed certificate case. + const httpsAgent = new https.Agent({ rejectUnauthorized: false }) + + this.axios = axios.create({ + httpsAgent + }) + } + + public static getInstance(cheApiEndpoint: string): CheApiClient { + cheApiEndpoint = this.normalizeCheApiEndpointUrl(cheApiEndpoint)! + if (!instance || instance.cheApiEndpoint !== cheApiEndpoint) { + instance = new CheApiClient(cheApiEndpoint) + } + return instance + } + + private static normalizeCheApiEndpointUrl(url: string | undefined) { + if (url) { + if (!url.includes('://')) { + url = 'https://' + url + } + const u = new URL(url) + url = 'https://' + u.host + u.pathname + if (url.endsWith('/')) { + url = url.slice(0, -1) + } + return url + } + } + + /** + * Checks whether provided url really points to Che server API. + * Throws an exception if it's not. + */ + async checkCheApiEndpointUrl(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + try { + const response = await this.axios.get(`${this.cheApiEndpoint}/system/state`, { timeout: responseTimeoutMs }) + if (response.data && response.data.status) { + return + } + } catch { + throw new Error(`E_CHE_API_URL_NO_RESPONSE - Failed to connect to "${this.cheApiEndpoint}". Is it the right url?`) + } + throw new Error(`E_CHE_API_WRONG_URL - Provided url "${this.cheApiEndpoint}" is not Che API url`) + } + + async isCheServerReady(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const id = this.axios.interceptors.response.use(response => response, async (error: any) => { + if (error.config && error.response && (error.response.status === 404 || error.response.status === 503)) { + await sleep(500) + return this.axios.request(error.config) + } + return Promise.reject(error) + }) + + try { + await this.axios.get(`${this.cheApiEndpoint}/system/state`, { timeout: responseTimeoutMs }) + return true + } catch { + return false + } finally { + this.axios.interceptors.response.eject(id) + } + } + + async getCheServerStatus(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const endpoint = `${this.cheApiEndpoint}/system/state` + let response = null + try { + response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) + } catch (error) { + throw this.getCheApiError(error, endpoint) + } + if (!response || response.status !== 200 || !response.data || !response.data.status) { + throw new Error('E_BAD_RESP_CHE_API') + } + return response.data.status + } + + async startCheServerShutdown(accessToken = '', responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const endpoint = `${this.cheApiEndpoint}/system/stop?shutdown=true` + const headers = accessToken ? { Authorization: accessToken } : null + let response = null + try { + response = await this.axios.post(endpoint, null, { headers, timeout: responseTimeoutMs }) + } catch (error) { + if (error.response && error.response.status === 409) { + return + } else { + throw this.getCheApiError(error, endpoint) + } + } + if (!response || response.status !== 204) { + throw new Error('E_BAD_RESP_CHE_API') + } + } + + async waitUntilCheServerReadyToShutdown(intervalMs = 500, timeoutMs = 60000): Promise { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + let status = await this.getCheServerStatus() + if (status === 'READY_TO_SHUTDOWN') { + return + } + await cli.wait(intervalMs) + } + throw new Error('ERR_TIMEOUT') + } + + /** + * Returns list of all workspaces of the user. + */ + async getAllWorkspaces(accessToken?: string): Promise { + const all: chetypes.workspace.Workspace[] = [] + const itemsPerPage = 30 + + let skipCount = 0 + let workspaces: chetypes.workspace.Workspace[] + do { + workspaces = await this.getWorkspaces(skipCount, itemsPerPage, accessToken) + all.push(...workspaces) + skipCount += workspaces.length + } while (workspaces.length === itemsPerPage) + + return all + } + + /** + * Returns list of workspaces in given range. + * If lst of all workspaces is needed, getAllWorkspaces should be used insted. + */ + async getWorkspaces(skipCount = 0, maxItems = 30, accessToken?: string): Promise { + const endpoint = `${this.cheApiEndpoint}/workspace?skipCount=${skipCount}&maxItems=${maxItems}` + const headers: any = { 'Content-Type': 'text/yaml' } + if (accessToken && accessToken.length > 0) { + headers.Authorization = accessToken + } + + try { + const response = await this.axios.get(endpoint, { headers }) + if (response && response.data) { + return response.data + } else { + throw new Error('E_BAD_RESP_CHE_SERVER') + } + } catch (error) { + throw this.getCheApiError(error, endpoint) + } + } + + async getWorkspaceById(workspaceId: string, accessToken?: string): Promise { + const endpoint = `${this.cheApiEndpoint}/workspace/${workspaceId}` + const headers: any = { 'Content-Type': 'text/yaml' } + if (accessToken) { + headers.Authorization = accessToken + } + + try { + const response = await this.axios.get(endpoint, { headers }) + return response.data + } catch (error) { + if (error.response.status === 404) { + throw new Error(`Workspace ${workspaceId} not found. Please use the command workspace:list to get list of the existed workspaces.`) + } + throw this.getCheApiError(error, endpoint) + } + } + + async deleteWorkspaceById(workspaceId: string, accessToken?: string): Promise { + const endpoint = `${this.cheApiEndpoint}/workspace/${workspaceId}` + const headers: any = {} + if (accessToken) { + headers.Authorization = accessToken + } + + try { + await this.axios.delete(endpoint, { headers }) + } catch (error) { + if (error.response.status === 404) { + throw new Error(`Workspace ${workspaceId} not found. Please use the command workspace:list to get list of the existed workspaces.`) + } else if (error.response.status === 409) { + throw new Error('Cannot delete a running workspace. Please stop it using the command workspace:stop and try again') + } + throw this.getCheApiError(error, endpoint) + } + } + + async startWorkspace(workspaceId: string, debug: boolean, accessToken?: string): Promise { + let endpoint = `${this.cheApiEndpoint}/workspace/${workspaceId}/runtime` + if (debug) { + endpoint += '?debug-workspace-start=true' + } + let response + + const headers: { [key: string]: string } = {} + if (accessToken) { + headers.Authorization = accessToken + } + try { + response = await this.axios.post(endpoint, undefined, { headers }) + } catch (error) { + if (error.response && error.response.status === 404) { + throw new Error(`E_WORKSPACE_NOT_EXIST - workspace with "${workspaceId}" id doesn't exist`) + } else { + throw this.getCheApiError(error, endpoint) + } + } + + if (!response || response.status !== 200 || !response.data) { + throw new Error('E_BAD_RESP_CHE_API') + } + } + + async stopWorkspace(workspaceId: string, accessToken?: string): Promise { + const endpoint = `${this.cheApiEndpoint}/workspace/${workspaceId}/runtime` + let response + + const headers: { [key: string]: string } = {} + if (accessToken) { + headers.Authorization = accessToken + } + try { + response = await this.axios.delete(endpoint, { headers }) + } catch (error) { + if (error.response && error.response.status === 404) { + throw new Error(`E_WORKSPACE_NOT_EXIST - workspace with "${workspaceId}" id doesn't exist`) + } else { + throw this.getCheApiError(error, endpoint) + } + } + + if (!response || response.status !== 204) { + throw new Error('E_BAD_RESP_CHE_API') + } + } + + async createWorkspaceFromDevfile(devfileContent: string, accessToken?: string): Promise { + const endpoint = `${this.cheApiEndpoint}/workspace/devfile` + const headers: any = { 'Content-Type': 'text/yaml' } + if (accessToken) { + headers.Authorization = accessToken + } + + let response: any + try { + response = await this.axios.post(endpoint, devfileContent, { headers }) + } catch (error) { + if (error.response) { + if (error.response.status === 400) { + throw new Error(`E_BAD_DEVFILE_FORMAT - Message: ${error.response.data.message}`) + } + if (error.response.status === 409) { + let message = '' + if (error.response.data) { + message = error.response.data.message + } + throw new Error(`E_CONFLICT - Message: ${message}`) + } + } + + throw this.getCheApiError(error, endpoint) + } + + if (response && response.data) { + return response.data as chetypes.workspace.Workspace + } else { + throw new Error('E_BAD_RESP_CHE_SERVER') + } + } + + async isAuthenticationEnabled(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const endpoint = `${this.cheApiEndpoint}/keycloak/settings` + let response = null + try { + response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) + } catch (error) { + if (error.response && (error.response.status === 404 || error.response.status === 503)) { + return false + } else { + throw this.getCheApiError(error, endpoint) + } + } + if (!response || response.status !== 200 || !response.data) { + throw new Error('E_BAD_RESP_CHE_API') + } + return true + } + + getCheApiError(error: any, endpoint: string): Error { + if (error.response) { + const status = error.response.status + if (status === 403) { + return new Error(`E_CHE_API_FORBIDDEN - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data.message)}`) + } else if (status === 401) { + return new Error(`E_CHE_API_UNAUTHORIZED - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`) + } else if (status === 404) { + return new Error(`E_CHE_API_NOTFOUND - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`) + } else { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + return new Error(`E_CHE_API_UNKNOWN_ERROR - Endpoint: ${endpoint} -Status: ${error.response.status}`) + } + + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + return new Error(`E_CHE_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`) + } else { + // Something happened in setting up the request that triggered an Error + return new Error(`E_CHECTL_UNKNOWN_ERROR - Endpoint: ${endpoint} - Message: ${error.message}`) + } + } + +} diff --git a/src/api/che.ts b/src/api/che.ts index 9dbcaa136..815834615 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -12,7 +12,6 @@ import { che as chetypes } from '@eclipse-che/api' import { CoreV1Api, V1Pod, Watch } from '@kubernetes/client-node' import axios, { AxiosInstance } from 'axios' import * as cp from 'child_process' -import { cli } from 'cli-ux' import * as commandExists from 'command-exists' import * as fs from 'fs-extra' import * as https from 'https' @@ -24,6 +23,7 @@ import { OpenShiftHelper } from '../api/openshift' import { CHE_ROOT_CA_SECRET_NAME, DEFAULT_CA_CERT_FILE_NAME } from '../constants' import { base64Decode } from '../util' +import { CheApiClient } from './che-api-client' import { Devfile } from './devfile' import { KubeHelper } from './kube' @@ -205,131 +205,40 @@ export class CheHelper { return this.kube.namespaceExist(namespace) } + /** + * DEPRECATED. Use CheApiClient instead. + */ async getCheServerStatus(cheURL: string, responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { - const endpoint = `${cheURL}/api/system/state` - let response = null - try { - response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) - } catch (error) { - throw this.getCheApiError(error, endpoint) - } - if (!response || response.status !== 200 || !response.data || !response.data.status) { - throw new Error('E_BAD_RESP_CHE_API') - } - return response.data.status + const cheApi = CheApiClient.getInstance(cheURL + '/api') + return cheApi.getCheServerStatus(responseTimeoutMs) } + /** + * DEPRECATED. Use startCheServerShutdown from CheApiClient instead. + */ async startShutdown(cheURL: string, accessToken = '', responseTimeoutMs = this.defaultCheResponseTimeoutMs) { - const endpoint = `${cheURL}/api/system/stop?shutdown=true` - const headers = accessToken ? { Authorization: `${accessToken}` } : null - let response = null - try { - response = await this.axios.post(endpoint, null, { headers, timeout: responseTimeoutMs }) - } catch (error) { - if (error.response && error.response.status === 409) { - return - } else { - throw this.getCheApiError(error, endpoint) - } - } - if (!response || response.status !== 204) { - throw new Error('E_BAD_RESP_CHE_API') - } + const cheApi = CheApiClient.getInstance(cheURL + '/api') + return cheApi.startCheServerShutdown(accessToken, responseTimeoutMs) } + /** + * DEPRECATED. Use waitUntilCheServerReadyToShutdown from CheApiClient instead. + */ async waitUntilReadyToShutdown(cheURL: string, intervalMs = 500, timeoutMs = 60000) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - let status = await this.getCheServerStatus(cheURL) - if (status === 'READY_TO_SHUTDOWN') { - return - } - await cli.wait(intervalMs) - } - throw new Error('ERR_TIMEOUT') + const cheApi = CheApiClient.getInstance(cheURL + '/api') + return cheApi.waitUntilCheServerReadyToShutdown(intervalMs, timeoutMs) } + /** + * DEPRECATED. Use CheApiClient instead. + */ async isCheServerReady(cheURL: string, responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { - const id = await this.axios.interceptors.response.use(response => response, async (error: any) => { - if (error.config && error.response && (error.response.status === 404 || error.response.status === 503)) { - return this.axios.request(error.config) - } - return Promise.reject(error) - }) - - try { - await this.axios.get(`${cheURL}/api/system/state`, { timeout: responseTimeoutMs }) - await this.axios.interceptors.response.eject(id) - return true - } catch { - await this.axios.interceptors.response.eject(id) - return false - } - } - - async startWorkspace(cheNamespace: string, workspaceId: string, debug: boolean, accessToken: string | undefined): Promise { - const cheUrl = await this.cheURL(cheNamespace) - let endpoint = `${cheUrl}/api/workspace/${workspaceId}/runtime` - if (debug) { - endpoint += '?debug-workspace-start=true' - } - let response - - const headers: { [key: string]: string } = {} - if (accessToken) { - headers.Authorization = accessToken - } - try { - response = await this.axios.post(endpoint, undefined, { headers }) - } catch (error) { - if (error.response && error.response.status === 404) { - throw new Error(`E_WORKSPACE_NOT_EXIST - workspace with "${workspaceId}" id doesn't exist`) - } else { - throw this.getCheApiError(error, endpoint) - } - } - - if (!response || response.status !== 200 || !response.data) { - throw new Error('E_BAD_RESP_CHE_API') - } + const cheApi = CheApiClient.getInstance(cheURL + '/api') + return cheApi.isCheServerReady(responseTimeoutMs) } - async stopWorkspace(cheUrl: string, workspaceId: string, accessToken?: string): Promise { - let endpoint = `${cheUrl}/api/workspace/${workspaceId}/runtime` - let response - - const headers: { [key: string]: string } = {} - if (accessToken) { - headers.Authorization = accessToken - } - try { - response = await this.axios.delete(endpoint, { headers }) - } catch (error) { - if (error.response && error.response.status === 404) { - throw new Error(`E_WORKSPACE_NOT_EXIST - workspace with "${workspaceId}" id doesn't exist`) - } else { - throw this.getCheApiError(error, endpoint) - } - } - - if (!response || response.status !== 204) { - throw new Error('E_BAD_RESP_CHE_API') - } - } - - async createWorkspaceFromDevfile(namespace: string | undefined, devfilePath = '', workspaceName: string | undefined, accessToken = ''): Promise { - if (!await this.cheNamespaceExist(namespace)) { - throw new Error('E_BAD_NS') - } - let url = await this.cheURL(namespace) - let endpoint = `${url}/api/workspace/devfile` + async createWorkspaceFromDevfile(cheApiEndpoint: string, devfilePath: string, workspaceName?: string, accessToken?: string): Promise { let devfile: string | undefined - let response - const headers: any = { 'Content-Type': 'text/yaml' } - if (accessToken && accessToken.length > 0) { - headers.Authorization = `${accessToken}` - } - try { devfile = await this.parseDevfile(devfilePath) if (workspaceName) { @@ -337,34 +246,14 @@ export class CheHelper { json.metadata.name = workspaceName devfile = yaml.dump(json) } - - response = await this.axios.post(endpoint, devfile, { headers }) } catch (error) { if (!devfile) { throw new Error(`E_NOT_FOUND_DEVFILE - ${devfilePath} - ${error.message}`) } - - if (error.response) { - if (error.response.status === 400) { - throw new Error(`E_BAD_DEVFILE_FORMAT - Message: ${error.response.data.message}`) - } - if (error.response.status === 409) { - let message = '' - if (error.response.data) { - message = error.response.data.message - } - throw new Error(`E_CONFLICT - Message: ${message}`) - } - } - - throw this.getCheApiError(error, endpoint) } - if (response && response.data) { - return response.data as chetypes.workspace.Workspace - } else { - throw new Error('E_BAD_RESP_CHE_SERVER') - } + const cheApi = CheApiClient.getInstance(cheApiEndpoint) + return cheApi.createWorkspaceFromDevfile(devfile, accessToken) } async parseDevfile(devfilePath = ''): Promise { @@ -376,22 +265,12 @@ export class CheHelper { } } + /** + * DEPRECATED. Use CheApiClient instead. + */ async isAuthenticationEnabled(cheURL: string, responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { - const endpoint = `${cheURL}/api/keycloak/settings` - let response = null - try { - response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) - } catch (error) { - if (error.response && (error.response.status === 404 || error.response.status === 503)) { - return false - } else { - throw this.getCheApiError(error, endpoint) - } - } - if (!response || response.status !== 200 || !response.data) { - throw new Error('E_BAD_RESP_CHE_API') - } - return true + const cheApi = CheApiClient.getInstance(cheURL + '/api') + return cheApi.isAuthenticationEnabled(responseTimeoutMs) } async buildDashboardURL(ideURL: string): Promise { @@ -505,85 +384,6 @@ 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 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) { - headers.Authorization = `${accessToken}` - } - - try { - const response = await this.axios.get(endpoint, { headers }) - if (response && response.data) { - return response.data - } else { - throw new Error('E_BAD_RESP_CHE_SERVER') - } - } catch (error) { - throw this.getCheApiError(error, endpoint) - } - } - - /** - * Get workspace. - */ - async getWorkspace(cheUrl: string, workspaceId: string, accessToken = ''): Promise { - const endpoint = `${cheUrl}/api/workspace/${workspaceId}` - const headers: any = { 'Content-Type': 'text/yaml' } - if (accessToken) { - headers.Authorization = `${accessToken}` - } - - try { - const response = await this.axios.get(endpoint, { headers }) - return response.data - } catch (error) { - if (error.response.status === 404) { - throw new Error(`Workspace ${workspaceId} not found. Please use the command workspace:list to get list of the existed workspaces.`) - } - throw this.getCheApiError(error, endpoint) - } - } - - /** - * Deletes workspace. - */ - async deleteWorkspace(cheUrl: string, workspaceId: string, accessToken = ''): Promise { - const endpoint = `${cheUrl}/api/workspace/${workspaceId}` - const headers: any = {} - if (accessToken) { - headers.Authorization = `${accessToken}` - } - - try { - await this.axios.delete(endpoint, { headers }) - } catch (error) { - if (error.response.status === 404) { - throw new Error(`Workspace ${workspaceId} not found. Please use the command workspace:list to get list of the existed workspaces.`) - } else if (error.response.status === 409) { - throw new Error('Cannot delete a running workspace. Please stop it using the command workspace:stop and try again') - } - throw this.getCheApiError(error, endpoint) - } - } - /** * Indicates if pod matches given labels. */ @@ -634,29 +434,4 @@ export class CheHelper { return fileName } - getCheApiError(error: any, endpoint: string): Error { - if (error.response) { - const status = error.response.status - if (status === 403) { - return new Error(`E_CHE_API_FORBIDDEN - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data.message)}`) - } else if (status === 401) { - return new Error(`E_CHE_API_UNAUTHORIZED - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`) - } else if (status === 404) { - return new Error(`E_CHE_API_NOTFOUND - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`) - } else { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - return new Error(`E_CHE_API_UNKNOWN_ERROR - Endpoint: ${endpoint} -Status: ${error.response.status}`) - } - - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - return new Error(`E_CHE_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`) - } else { - // Something happened in setting up the request that triggered an Error - return new Error(`E_CHECTL_UNKNOWN_ERROR - Endpoint: ${endpoint} - Message: ${error.message}`) - } - } } diff --git a/src/api/kube.ts b/src/api/kube.ts index 07d592478..61d6ed906 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, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' +import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, AuthorizationV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1SelfSubjectAccessReview, V1SelfSubjectAccessReviewSpec, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' import { Cluster, Context } from '@kubernetes/client-node/dist/config_types' import axios, { AxiosRequestConfig } from 'axios' import { cli } from 'cli-ux' @@ -555,6 +555,31 @@ export class KubeHelper { } } + async hasReadPermissionsForNamespace(namespace: string): Promise { + const k8sApi = KubeHelper.KUBE_CONFIG.makeApiClient(AuthorizationV1Api) + const accessReview = new V1SelfSubjectAccessReview() + accessReview.spec = new V1SelfSubjectAccessReviewSpec() + accessReview.spec.resourceAttributes = { + group: '', + name: 'access-to-che-namespace', + namespace, + resource: 'namespaces', + verb: 'get' + } + + try { + const { body } = await k8sApi.createSelfSubjectAccessReview(accessReview) + return body.status!.allowed + } catch (error) { + if (error.response && error.response.body) { + if (error.response.body.code === 403) { + return false + } + } + throw this.wrapK8sClientError(error) + } + } + async readNamespacedPod(podName: string, namespace: string): Promise { const k8sCoreApi = KubeHelper.KUBE_CONFIG.makeApiClient(CoreV1Api) try { diff --git a/src/commands/workspace/create.ts b/src/commands/workspace/create.ts index 7671e1524..a4e463eef 100644 --- a/src/commands/workspace/create.ts +++ b/src/commands/workspace/create.ts @@ -11,13 +11,13 @@ import { Command, flags } from '@oclif/command' import { boolean, string } from '@oclif/parser/lib/flags' import { cli } from 'cli-ux' -import * as Listr from 'listr' +import * as fs from 'fs' import * as notifier from 'node-notifier' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' -import { WorkspaceTasks } from '../../tasks/workspace-tasks' +import { CheHelper } from '../../api/che' +import { CheApiClient } from '../../api/che-api-client' +import { KubeHelper } from '../../api/kube' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' export default class Create extends Command { static description = 'Creates a workspace from a devfile' @@ -45,41 +45,44 @@ export default class Create extends Command { description: 'Debug workspace start. It is useful when workspace start fails and it is needed to print more logs on startup. This flag is used in conjunction with --start flag.', default: false }), - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, 'skip-kubernetes-health-check': skipKubeHealthzCheck } async run() { const { flags } = this.parse(Create) - const ctx: any = {} - const apiTasks = new ApiTasks() - const cheTasks = new CheTasks(flags) - const workspaceTasks = new WorkspaceTasks(flags) + const devfilePath = this.getDevfilePath(flags.devfile) + const accessToken = flags[ACCESS_TOKEN_KEY] + const cheHelper = new CheHelper(flags) - const tasks = new Listr([], { renderer: 'silent' }) + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) + } + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' + } + + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() + + let workspace = await cheHelper.createWorkspaceFromDevfile(cheApiEndpoint, devfilePath, flags.name, accessToken) + const workspaceId = workspace.id! - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.retrieveEclipseCheUrl(flags)) - tasks.add(cheTasks.checkEclipseCheStatus()) - tasks.add(workspaceTasks.getWorkspaceCreateTask(flags.devfile, flags.name)) if (flags.start) { - tasks.add(workspaceTasks.getWorkspaceStartTask(flags.debug)) + await cheApiClient.startWorkspace(workspaceId, flags.debug, accessToken) + this.log('Workspace has been successfully created and workspace start request has been sent.') + this.log('Workspace will be available shortly:') + } else { + this.log('Workspace has been successfully created:') } - tasks.add(workspaceTasks.getWorkspaceIdeUrlTask()) - - try { - await tasks.run(ctx) - if (flags.start) { - this.log('Workspace has been successfully created and workspace start request has been sent') - this.log('Workspace will be available shortly:') - } else { - this.log('Workspace has been successfully created:') - } - cli.url(ctx.workspaceIdeURL, ctx.workspaceIdeURL) - } catch (err) { - this.error(err) + workspace = await cheApiClient.getWorkspaceById(workspaceId, accessToken) + if (workspace.links && workspace.links.ide) { + const workspaceIdeURL = await cheHelper.buildDashboardURL(workspace.links.ide) + cli.url(workspaceIdeURL, workspaceIdeURL) } notifier.notify({ @@ -89,4 +92,18 @@ export default class Create extends Command { this.exit(0) } + + private getDevfilePath(devfilePath?: string) { + if (!devfilePath) { + if (fs.existsSync('devfile.yaml')) { + devfilePath = 'devfile.yaml' + } else if (fs.existsSync('devfile.yml')) { + devfilePath = 'devfile.yml' + } else { + throw new Error("E_DEVFILE_MISSING - Devfile wasn't specified via '-f' option and 'devfile.yaml' is not present in current directory.") + } + } + return devfilePath + } + } diff --git a/src/commands/workspace/delete.ts b/src/commands/workspace/delete.ts index 22b128b95..4352ff56a 100644 --- a/src/commands/workspace/delete.ts +++ b/src/commands/workspace/delete.ts @@ -10,14 +10,12 @@ import { Command, flags } from '@oclif/command' import { cli } from 'cli-ux' -import * as Listrq from 'listr' import * as notifier from 'node-notifier' import { CheHelper } from '../../api/che' +import { CheApiClient } from '../../api/che-api-client' import { KubeHelper } from '../../api/kube' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' export default class Delete extends Command { static description = 'delete a stopped workspace - use workspace:stop to stop the workspace before deleting it' @@ -29,7 +27,8 @@ export default class Delete extends Command { description: 'Indicates that a Kubernetes namespace where workspace was created will be deleted as well', default: false }), - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, 'skip-kubernetes-health-check': skipKubeHealthzCheck } static args = [ @@ -43,70 +42,44 @@ export default class Delete extends Command { async run() { const { flags } = this.parse(Delete) const { args } = this.parse(Delete) - const ctx: any = {} - ctx.workspaces = [] - const apiTasks = new ApiTasks() - const cheTasks = new CheTasks(flags) - const cheHelper = new CheHelper(flags) - const kubeHelper = new KubeHelper(flags) - const tasks = new Listrq(undefined, { renderer: 'silent' }) + const workspaceId = args.workspace - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.retrieveEclipseCheUrl(flags)) - tasks.add(cheTasks.checkEclipseCheStatus()) - - tasks.add({ - title: `Get workspace by id '${args.workspace}'`, - task: async (ctx, task) => { - const workspace = await cheHelper.getWorkspace(ctx.cheURL, args.workspace, flags['access-token']) - ctx.infrastructureNamespace = workspace.attributes.infrastructureNamespace - task.title = `${task.title}... done` + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) } - }) - tasks.add({ - title: `Delete workspace by id '${args.workspace}'`, - task: async (ctx, task) => { - await cheHelper.deleteWorkspace(ctx.cheURL, args.workspace, flags['access-token']) - cli.log(`Workspace with id '${args.workspace}' deleted.`) - task.title = `${task.title}... done` + + const cheHelper = new CheHelper(flags) + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' + } + + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() + + const workspace = await cheApiClient.getWorkspaceById(workspaceId, flags[ACCESS_TOKEN_KEY]) + const infrastructureNamespace = workspace!.attributes!.infrastructureNamespace + + await cheApiClient.deleteWorkspaceById(workspaceId, flags[ACCESS_TOKEN_KEY]) + cli.log(`Workspace with id '${workspaceId}' deleted.`) + + if (flags['delete-namespace']) { + if (infrastructureNamespace === flags.chenamespace) { + cli.warn(`It is not possible to delete namespace '${infrastructureNamespace}' since it is used for Eclipse Che deployment.`) + return } - }) - tasks.add({ - title: 'Verify if namespace exists', - enabled: () => flags['delete-namespace'], - task: async (ctx, task) => { - task.title = `${task.title} '${ctx.infrastructureNamespace}'` - if (ctx.infrastructureNamespace === flags.chenamespace) { - cli.warn(`It is not possible to delete namespace '${ctx.infrastructureNamespace}' since it is used for Eclipse Che deployment.`) - return - } - ctx.infrastructureNamespaceExists = await kubeHelper.namespaceExist(ctx.infrastructureNamespace) - if (ctx.infrastructureNamespaceExists) { - task.title = `${task.title}... found` - } else { - task.title = `${task.title}... not found` + const kube = new KubeHelper(flags) + if (await kube.namespaceExist(infrastructureNamespace)) { + try { + await kube.deleteNamespace(infrastructureNamespace) + cli.log(`Namespace '${infrastructureNamespace}' deleted.`) + } catch (error) { + cli.warn(`Failed to delete namespace '${infrastructureNamespace}'. Reason: ${error.message}`) } } - }) - tasks.add({ - title: 'Delete namespace', - skip: ctx => !ctx.infrastructureNamespaceExists, - enabled: () => flags['delete-namespace'], - task: async (ctx, task) => { - task.title = `${task.title} '${ctx.infrastructureNamespace}'` - await kubeHelper.deleteNamespace(ctx.infrastructureNamespace) - cli.log(`Namespace '${ctx.infrastructureNamespace}' deleted.`) - task.title = `${task.title}... done` - } - }) - - try { - await tasks.run(ctx) - } catch (error) { - this.error(error) } notifier.notify({ diff --git a/src/commands/workspace/inject.ts b/src/commands/workspace/inject.ts index b30ddbe04..4b587826f 100644 --- a/src/commands/workspace/inject.ts +++ b/src/commands/workspace/inject.ts @@ -14,15 +14,13 @@ 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' import * as os from 'os' import * as path from 'path' import { CheHelper } from '../../api/che' +import { CheApiClient } from '../../api/che-api-client' import { KubeHelper } from '../../api/kube' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' import { getClusterClientCommand, OPENSHIFT_CLI } from '../../util' export default class Inject extends Command { @@ -49,7 +47,8 @@ export default class Inject extends Command { description: 'Kubeconfig context to inject', required: false }), - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, chenamespace: cheNamespace, 'skip-kubernetes-health-check': skipKubeHealthzCheck } @@ -61,49 +60,51 @@ export default class Inject extends Command { 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: 'silent' }) - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - - try { - await tasks.run() + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) + } + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' + } - let workspaceId = flags.workspace - let workspaceNamespace = '' + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() - 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 (!flags[ACCESS_TOKEN_KEY] && await cheApiClient.isAuthenticationEnabled()) { + 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.') - } + let workspaceId = flags.workspace + let workspaceNamespace = '' + if (!workspaceId) { + const workspaces = await cheApiClient.getAllWorkspaces(flags[ACCESS_TOKEN_KEY]) + 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 { - 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 + 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.') } - - 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.`) + } else { + const workspace = await cheApiClient.getWorkspaceById(workspaceId, flags[ACCESS_TOKEN_KEY]) + 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.`) + } + + try { await this.injectKubeconfig(flags, workspaceNamespace, workspacePodName, workspaceId!) } catch (err) { this.error(err) @@ -161,17 +162,17 @@ 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 */ - 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 }) + private async doInjectKubeconfig(namespace: string, workspacePod: string, container: string, contextToInject: Context): Promise { + const { stdout } = await execa(`${this.command} exec ${workspacePod} -n ${namespace} -c ${container} env | grep ^HOME=`, { timeout: 10000, shell: true }) let containerHomeDir = stdout.split('=')[1] if (!containerHomeDir.endsWith('/')) { containerHomeDir += '/' } - if (await this.fileExists(cheNamespace, workspacePod, container, `${containerHomeDir}.kube/config`)) { + if (await this.fileExists(namespace, workspacePod, container, `${containerHomeDir}.kube/config`)) { throw new Error('kubeconfig already exists in the target container') } - await execa(`${this.command} exec ${workspacePod} -n ${cheNamespace} -c ${container} -- mkdir ${containerHomeDir}.kube -p`, { timeout: 10000, shell: true }) + await execa(`${this.command} exec ${workspacePod} -n ${namespace} -c ${container} -- mkdir ${containerHomeDir}.kube -p`, { timeout: 10000, shell: true }) const kubeConfigPath = path.join(os.tmpdir(), 'che-kubeconfig') const cluster = KubeHelper.KUBE_CONFIG.getCluster(contextToInject.cluster) @@ -213,10 +214,10 @@ export default class Inject extends Command { } await execa(this.command, setCredentialsArgs, { timeout: 10000 }) - await execa(this.command, ['config', configPathFlag, kubeConfigPath, 'set-context', contextToInject.name, `--cluster=${contextToInject.cluster}`, `--user=${contextToInject.user}`, `--namespace=${cheNamespace}`], { timeout: 10000 }) + await execa(this.command, ['config', configPathFlag, kubeConfigPath, 'set-context', contextToInject.name, `--cluster=${contextToInject.cluster}`, `--user=${contextToInject.user}`, `--namespace=${namespace}`], { timeout: 10000 }) await execa(this.command, ['config', configPathFlag, kubeConfigPath, 'use-context', contextToInject.name], { timeout: 10000 }) - await execa(this.command, ['cp', kubeConfigPath, `${cheNamespace}/${workspacePod}:${containerHomeDir}.kube/config`, '-c', container], { timeout: 10000 }) + await execa(this.command, ['cp', kubeConfigPath, `${namespace}/${workspacePod}:${containerHomeDir}.kube/config`, '-c', container], { timeout: 10000 }) return } diff --git a/src/commands/workspace/list.ts b/src/commands/workspace/list.ts index c313e050a..f045e672a 100644 --- a/src/commands/workspace/list.ts +++ b/src/commands/workspace/list.ts @@ -10,12 +10,11 @@ import { Command, flags } from '@oclif/command' import { cli } from 'cli-ux' -import * as Listrq from 'listr' import { CheHelper } from '../../api/che' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' +import { CheApiClient } from '../../api/che-api-client' +import { KubeHelper } from '../../api/kube' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY as CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' export default class List extends Command { static description = 'list workspaces' @@ -23,41 +22,35 @@ export default class List extends Command { static flags = { help: flags.help({ char: 'h' }), chenamespace: cheNamespace, - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, 'skip-kubernetes-health-check': skipKubeHealthzCheck } async run() { const { flags } = this.parse(List) - const ctx: any = {} - ctx.workspaces = [] - const apiTasks = new ApiTasks() - const cheTasks = new CheTasks(flags) - const tasks = new Listrq(undefined, { renderer: 'silent' }) - - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.retrieveEclipseCheUrl(flags)) - tasks.add(cheTasks.checkEclipseCheStatus()) - tasks.add({ - title: 'Get workspaces', - task: async (ctx, task) => { - const cheHelper = new CheHelper(flags) - ctx.workspaces = await cheHelper.getAllWorkspaces(ctx.cheURL, flags['access-token']) - task.title = `${task.title}... done` + let workspaces = [] + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) } - }) - try { - await tasks.run(ctx) - this.printWorkspaces(ctx.workspaces) - } catch (error) { - this.error(error.message) + const cheHelper = new CheHelper(flags) + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' } + + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() + + workspaces = await cheApiClient.getAllWorkspaces(flags[ACCESS_TOKEN_KEY]) + + this.printWorkspaces(workspaces) } - private printWorkspaces(workspaces: [any]): void { + private printWorkspaces(workspaces: any[]): void { const data: any[] = [] workspaces.forEach((workspace: any) => { data.push({ diff --git a/src/commands/workspace/logs.ts b/src/commands/workspace/logs.ts index f8e56f30f..62a7602ec 100644 --- a/src/commands/workspace/logs.ts +++ b/src/commands/workspace/logs.ts @@ -10,21 +10,18 @@ import { Command, flags } from '@oclif/command' import { string } from '@oclif/parser/lib/flags' -import * as Listr from 'listr' import * as notifier from 'node-notifier' import * as os from 'os' import * as path from 'path' -import { listrRenderer, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' +import { CheHelper } from '../../api/che' +import { skipKubeHealthzCheck } from '../../common-flags' export default class Logs extends Command { static description = 'Collect workspace(s) logs' static flags = { help: flags.help({ char: 'h' }), - 'listr-renderer': listrRenderer, workspace: string({ char: 'w', description: 'Target workspace id. Can be found in workspace configuration \'id\' field.', @@ -44,21 +41,16 @@ export default class Logs extends Command { } async run() { - const ctx: any = {} const { flags } = this.parse(Logs) - ctx.directory = path.resolve(flags.directory ? flags.directory : path.resolve(os.tmpdir(), 'chectl-logs', Date.now().toString())) - const cheTasks = new CheTasks(flags) - const apiTasks = new ApiTasks() + const logsDirectory = path.resolve(flags.directory ? flags.directory : path.resolve(os.tmpdir(), 'chectl-logs', Date.now().toString())) - const tasks = new Listr([], { renderer: flags['listr-renderer'] as any }) - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.workspaceLogsTasks(flags.namespace, flags.workspace)) + const cheHelper = new CheHelper(flags) + const workspaceRun = await cheHelper.readWorkspacePodLog(flags.namespace, flags.workspace, logsDirectory) try { - this.log(`Eclipse Che logs will be available in '${ctx.directory}'`) - await tasks.run(ctx) + this.log(`Eclipse Che logs will be available in '${logsDirectory}'`) - if (!ctx['workspace-run']) { + if (!workspaceRun) { 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...') diff --git a/src/commands/workspace/start.ts b/src/commands/workspace/start.ts index d7450aa99..a7d770721 100644 --- a/src/commands/workspace/start.ts +++ b/src/commands/workspace/start.ts @@ -10,13 +10,12 @@ import Command, { flags } from '@oclif/command' import { cli } from 'cli-ux' -import Listr = require('listr') import * as notifier from 'node-notifier' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' -import { WorkspaceTasks } from '../../tasks/workspace-tasks' +import { CheHelper } from '../../api/che' +import { CheApiClient } from '../../api/che-api-client' +import { KubeHelper } from '../../api/kube' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' export default class Start extends Command { static description = 'Starts a workspace' @@ -28,7 +27,8 @@ export default class Start extends Command { description: 'Debug workspace start. It is useful when workspace start fails and it is needed to print more logs on startup.', default: false }), - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, chenamespace: cheNamespace, 'skip-kubernetes-health-check': skipKubeHealthzCheck } @@ -44,28 +44,31 @@ export default class Start extends Command { async run() { const { flags } = this.parse(Start) const { args } = this.parse(Start) - const ctx: any = {} - const tasks = new Listr([], { renderer: 'silent' }) + const workspaceId = args.workspace + const cheHelper = new CheHelper(flags) - const apiTasks = new ApiTasks() - const cheTasks = new CheTasks(flags) - const workspaceTasks = new WorkspaceTasks(flags) + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) + } + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' + } + + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() - ctx.workspaceId = args.workspace - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.retrieveEclipseCheUrl(flags)) - tasks.add(cheTasks.checkEclipseCheStatus()) - tasks.add(workspaceTasks.getWorkspaceStartTask(flags.debug)) - tasks.add(workspaceTasks.getWorkspaceIdeUrlTask()) + await cheApiClient.startWorkspace(workspaceId, flags.debug, flags[ACCESS_TOKEN_KEY]) - try { - await tasks.run(ctx) - this.log('Workspace start request has been sent, workspace will be available shortly:') - cli.url(ctx.workspaceIdeURL, ctx.workspaceIdeURL) - } catch (err) { - this.error(err) + const workspace = await cheApiClient.getWorkspaceById(workspaceId, flags[ACCESS_TOKEN_KEY]) + if (workspace.links && workspace.links.ide) { + const workspaceIdeURL = await cheHelper.buildDashboardURL(workspace.links.ide) + cli.log('Workspace start request has been sent, workspace will be available shortly:') + cli.url(workspaceIdeURL, workspaceIdeURL) + } else { + cli.log('Workspace start request has been sent, workspace will be available shortly.') } notifier.notify({ diff --git a/src/commands/workspace/stop.ts b/src/commands/workspace/stop.ts index e7d4047cb..0f8baa2ba 100644 --- a/src/commands/workspace/stop.ts +++ b/src/commands/workspace/stop.ts @@ -10,20 +10,20 @@ import { Command, flags } from '@oclif/command' import { cli } from 'cli-ux' -import Listr = require('listr') import * as notifier from 'node-notifier' -import { accessToken, cheNamespace, skipKubeHealthzCheck } from '../../common-flags' -import { CheTasks } from '../../tasks/che' -import { ApiTasks } from '../../tasks/platforms/api' -import { WorkspaceTasks } from '../../tasks/workspace-tasks' +import { CheHelper } from '../../api/che' +import { CheApiClient } from '../../api/che-api-client' +import { KubeHelper } from '../../api/kube' +import { accessToken, ACCESS_TOKEN_KEY, cheApiEndpoint, cheNamespace, CHE_API_ENDPOINT_KEY, skipKubeHealthzCheck } from '../../common-flags' export default class Stop extends Command { static description = 'Stop a running workspace' static flags = { help: flags.help({ char: 'h' }), - 'access-token': accessToken, + [CHE_API_ENDPOINT_KEY]: cheApiEndpoint, + [ACCESS_TOKEN_KEY]: accessToken, chenamespace: cheNamespace, 'skip-kubernetes-health-check': skipKubeHealthzCheck } @@ -39,28 +39,26 @@ export default class Stop extends Command { async run() { const { flags } = this.parse(Stop) const { args } = this.parse(Stop) - const ctx: any = {} - const tasks = new Listr([], { renderer: 'silent' }) + const workspaceId = args.workspace - const apiTasks = new ApiTasks() - const cheTasks = new CheTasks(flags) - const workspaceTasks = new WorkspaceTasks(flags) + let cheApiEndpoint = flags[CHE_API_ENDPOINT_KEY] + if (!cheApiEndpoint) { + const kube = new KubeHelper(flags) + if (!await kube.hasReadPermissionsForNamespace(flags.chenamespace)) { + throw new Error(`Eclipse Che API endpoint is required. Use flag --${CHE_API_ENDPOINT_KEY} to provide it.`) + } - ctx.workspaceId = args.workspace - tasks.add(apiTasks.testApiTasks(flags, this)) - tasks.add(cheTasks.verifyCheNamespaceExistsTask(flags, this)) - tasks.add(cheTasks.retrieveEclipseCheUrl(flags)) - tasks.add(cheTasks.checkEclipseCheStatus()) - tasks.add(workspaceTasks.getWorkspaceStopTask()) - - try { - await tasks.run(ctx) - cli.log('Workspace successfully stopped.') - } catch (err) { - this.error(err) + const cheHelper = new CheHelper(flags) + cheApiEndpoint = await cheHelper.cheURL(flags.chenamespace) + '/api' } + const cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + await cheApiClient.checkCheApiEndpointUrl() + + await cheApiClient.stopWorkspace(workspaceId, flags[ACCESS_TOKEN_KEY]) + cli.log('Workspace successfully stopped.') + notifier.notify({ title: 'chectl', message: 'Command workspace:stop has completed successfully.' diff --git a/src/common-flags.ts b/src/common-flags.ts index 9a5cc1036..c85759cca 100644 --- a/src/common-flags.ts +++ b/src/common-flags.ts @@ -30,6 +30,7 @@ export const listrRenderer = string({ default: 'default' }) +export const ACCESS_TOKEN_KEY = 'access-token' export const accessToken = string({ description: `Eclipse Che OIDC Access Token. See the documentation how to obtain token: ${DOC_LINK_OBTAIN_ACCESS_TOKEN} and ${DOC_LINK_OBTAIN_ACCESS_TOKEN_OAUTH}.`, env: 'CHE_ACCESS_TOKEN' @@ -39,3 +40,10 @@ export const skipKubeHealthzCheck = boolean({ description: 'Skip Kubernetes health check', default: false }) + +export const CHE_API_ENDPOINT_KEY = 'che-api-endpoint' +export const cheApiEndpoint = string({ + description: 'Eclipse Che server API endpoint', + env: 'CHE_API_ENDPOINT', + required: false, +}) diff --git a/src/tasks/che.ts b/src/tasks/che.ts index edecca41b..ab6e324d2 100644 --- a/src/tasks/che.ts +++ b/src/tasks/che.ts @@ -562,18 +562,6 @@ export class CheTasks { ] } - workspaceLogsTasks(namespace: string, workspaceId: string): ReadonlyArray { - return [ - { - title: 'Read workspace logs', - task: async (ctx: any, task: any) => { - ctx['workspace-run'] = await this.che.readWorkspacePodLog(namespace, workspaceId, ctx.directory) - task.title = `${task.title}...done` - } - } - ] - } - namespaceEventsTask(namespace: string, command: Command, follow: boolean): ReadonlyArray { return [ { diff --git a/src/tasks/workspace-tasks.ts b/src/tasks/workspace-tasks.ts deleted file mode 100644 index e22668b91..000000000 --- a/src/tasks/workspace-tasks.ts +++ /dev/null @@ -1,85 +0,0 @@ -/********************************************************************* - * Copyright (c) 2020 Red Hat, Inc. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - **********************************************************************/ - -import * as fs from 'fs' -import Listr = require('listr') - -import { CheHelper } from '../api/che' - -export class WorkspaceTasks { - cheHelper: CheHelper - cheNamespace: string - accessToken: string - constructor(flags: any) { - this.cheHelper = new CheHelper(flags) - this.cheNamespace = flags.chenamespace - this.accessToken = flags['access-token'] - } - - getWorkspaceStartTask(debug: boolean): ReadonlyArray { - return [ - { - title: 'Start the workspace', - task: async (ctx: any, task: any) => { - await this.cheHelper.startWorkspace(this.cheNamespace, ctx.workspaceId, debug, this.accessToken) - task.title = `${task.title}... Done` - } - } - ] - } - - getWorkspaceStopTask(): ReadonlyArray { - return [ - { - title: 'Stop the workspace', - task: async (ctx: any, task: any) => { - await this.cheHelper.stopWorkspace(ctx.cheURL, ctx.workspaceId, this.accessToken) - task.title = `${task.title}... Done` - } - } - ] - } - - getWorkspaceCreateTask(devfile: string | undefined, workspaceName: string | undefined): ReadonlyArray { - return [{ - title: 'Create a workspace from the Devfile', - task: async (ctx: any) => { - if (!devfile) { - if (fs.existsSync('devfile.yaml')) { - devfile = 'devfile.yaml' - } else if (fs.existsSync('devfile.yml')) { - devfile = 'devfile.yml' - } - } - - if (!devfile) { - throw new Error("E_DEVFILE_MISSING - Devfile wasn't specified via '-f' option and \'devfile.yaml' is not present in current directory.") - } - ctx.workspaceConfig = await this.cheHelper.createWorkspaceFromDevfile(this.cheNamespace, devfile, workspaceName, this.accessToken) - ctx.workspaceId = ctx.workspaceConfig.id - } - }] - } - - getWorkspaceIdeUrlTask(): ReadonlyArray { - return [ - { - title: 'Get the workspace IDE URL', - task: async (ctx: any, task: any) => { - const workspaceConfig = await this.cheHelper.getWorkspace(ctx.cheURL, ctx.workspaceId, this.accessToken) - if (workspaceConfig.links && workspaceConfig.links.ide) { - ctx.workspaceIdeURL = await this.cheHelper.buildDashboardURL(workspaceConfig.links.ide) - } - task.title = `${task.title}... Done` - } - } - ] - } -} diff --git a/src/util.ts b/src/util.ts index 25e6bff58..fdcf6ce84 100644 --- a/src/util.ts +++ b/src/util.ts @@ -90,3 +90,8 @@ export function getImageTag(image: string): string | undefined { // tag return entries[1] } + +export function sleep(ms: number): Promise { + // tslint:disable-next-line no-string-based-set-timeout + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/test/api/che-api-client.test.ts b/test/api/che-api-client.test.ts new file mode 100644 index 000000000..9422ecd29 --- /dev/null +++ b/test/api/che-api-client.test.ts @@ -0,0 +1,113 @@ +/********************************************************************* + * Copyright (c) 2020 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { expect, fancy } from 'fancy-test' + +import { CheApiClient } from '../../src/api/che-api-client' + +const cheApiEndpoint = 'https://che-che.192.168.64.34.nip.io/api' +const devfileEndpoint = '/workspace/devfile' +let cheApiClient = CheApiClient.getInstance(cheApiEndpoint) + +describe('Eclipse Che Server API client', () => { + describe('isCheServerReady', () => { + fancy + .nock(cheApiEndpoint, api => api + .get('/system/state') + .reply(200)) + .it('detects if Eclipse Che server is ready', async () => { + const res = await cheApiClient.isCheServerReady() + expect(res).to.equal(true) + }) + fancy + .nock(cheApiEndpoint, api => api + .get('/system/state') + .delayConnection(1000) + .reply(200)) + .it('detects if Eclipse Che server is NOT ready', async () => { + const res = await cheApiClient.isCheServerReady(500) + expect(res).to.equal(false) + }) + fancy + .nock(cheApiEndpoint, api => api + .get('/system/state') + .delayConnection(1000) + .reply(200)) + .it('waits until Eclipse Che server is ready', async () => { + const res = await cheApiClient.isCheServerReady(2000) + expect(res).to.equal(true) + }) + fancy + .nock(cheApiEndpoint, api => api + .get('/system/state') + .reply(404) + .get('/system/state') + .reply(503) + .get('/system/state') + .reply(200)) + .it('continues requesting until Eclipse Che server is ready', async () => { + const res = await cheApiClient.isCheServerReady(2000) + expect(res).to.equal(true) + }) + fancy + .nock(cheApiEndpoint, api => api + .get('/system/state') + .reply(404) + .get('/system/state') + .reply(404) + .get('/system/state') + .reply(503)) + .it('continues requesting but fails if Eclipse Che server is NOT ready after timeout', async () => { + const res = await cheApiClient.isCheServerReady(20) + expect(res).to.equal(false) + }) + }) + describe('createWorkspaceFromDevfile', () => { + fancy + .nock(cheApiEndpoint, api => api + .post(devfileEndpoint) + .replyWithFile(201, __dirname + '/replies/create-workspace-from-valid-devfile.json', { 'Content-Type': 'application/json' })) + .it('succeds creating a workspace from a valid devfile', async () => { + const res = await cheApiClient.createWorkspaceFromDevfile(__dirname + '/requests/devfile.valid') + expect(res.links!.ide).to.equal('https://che-che.192.168.64.39.nip.io/che/chectl') + }) + fancy + .nock(cheApiEndpoint, api => api + .post(devfileEndpoint) + .replyWithFile(400, __dirname + '/replies/create-workspace-from-invalid-devfile.json', { + 'Content-Type': 'application/json' + })) + .do(() => cheApiClient.createWorkspaceFromDevfile(__dirname + '/requests/devfile.invalid')) + .catch(/E_BAD_DEVFILE_FORMAT/) + .it('fails creating a workspace from an invalid devfile') + }) + describe('isAuthenticationEnabled', () => { + fancy + .nock(cheApiEndpoint, api => api + .get('/keycloak/settings') + .replyWithFile(200, __dirname + '/replies/get-keycloak-settings.json', { + 'Content-Type': 'application/json' + })) + .it('should return true if the api/keycloak/settings endpoint exist', async () => { + const authEnabled = await cheApiClient.isAuthenticationEnabled() + expect(authEnabled).to.equal(true) + }) + fancy + .nock(cheApiEndpoint, api => api + .get('/keycloak/settings') + .reply(404, 'Page does not exist', { + 'Content-Type': 'text/plain' + })) + .it('should return false if the api/keycloak/settings endpoint doesn\'t exist', async () => { + const authEnabled = await cheApiClient.isAuthenticationEnabled() + expect(authEnabled).to.equal(false) + }) + }) +}) diff --git a/test/api/che.test.ts b/test/api/che.test.ts index edcb76d3e..0c4e02dfc 100644 --- a/test/api/che.test.ts +++ b/test/api/che.test.ts @@ -11,6 +11,7 @@ import { CoreV1Api } from '@kubernetes/client-node' import { expect, fancy } from 'fancy-test' import { CheHelper } from '../../src/api/che' +// import { CheApiClient } from '../../src/api/che-api-client' import { KubeHelper } from '../../src/api/kube' const namespace = 'che' @@ -18,6 +19,7 @@ const workspace = 'workspace-0123' const cheURL = 'https://che-che.192.168.64.34.nip.io' const devfileServerURL = 'https://devfile-server' const devfileEndpoint = '/api/workspace/devfile' +// let cheApi = CheApiClient.getInstance(cheURL + '/api') let ch = new CheHelper({}) let kube = ch.kube let oc = ch.oc @@ -65,58 +67,6 @@ describe('Eclipse Che helper', () => { .catch(err => expect(err.message).to.match(/ERR_NAMESPACE_NO_EXIST/)) .it('fails fetching Eclipse Che URL when namespace does not exist') }) - describe('isCheServerReady', () => { - fancy - .nock(cheURL, api => api - .get('/api/system/state') - .reply(200)) - .it('detects if Eclipse Che server is ready', async () => { - const res = await ch.isCheServerReady(cheURL) - expect(res).to.equal(true) - }) - fancy - .nock(cheURL, api => api - .get('/api/system/state') - .delayConnection(1000) - .reply(200)) - .it('detects if Eclipse Che server is NOT ready', async () => { - const res = await ch.isCheServerReady(cheURL, 500) - expect(res).to.equal(false) - }) - fancy - .nock(cheURL, api => api - .get('/api/system/state') - .delayConnection(1000) - .reply(200)) - .it('waits until Eclipse Che server is ready', async () => { - const res = await ch.isCheServerReady(cheURL, 2000) - expect(res).to.equal(true) - }) - fancy - .nock(cheURL, api => api - .get('/api/system/state') - .reply(404) - .get('/api/system/state') - .reply(503) - .get('/api/system/state') - .reply(200)) - .it('continues requesting until Eclipse Che server is ready', async () => { - const res = await ch.isCheServerReady(cheURL, 2000) - expect(res).to.equal(true) - }) - fancy - .nock(cheURL, api => api - .get('/api/system/state') - .reply(404) - .get('/api/system/state') - .reply(404) - .get('/api/system/state') - .reply(503)) - .it('continues requesting but fails if Eclipse Che server is NOT ready after timeout', async () => { - const res = await ch.isCheServerReady(cheURL, 20) - expect(res).to.equal(false) - }) - }) describe('cheNamespaceExist', () => { fancy .stub(KubeHelper.KUBE_CONFIG, 'makeApiClient', () => k8sApi) @@ -134,27 +84,6 @@ describe('Eclipse Che helper', () => { }) }) describe('createWorkspaceFromDevfile', () => { - fancy - .stub(ch, 'cheNamespaceExist', () => true) - .stub(ch, 'cheURL', () => cheURL) - .nock(cheURL, api => api - .post(devfileEndpoint) - .replyWithFile(201, __dirname + '/replies/create-workspace-from-valid-devfile.json', { 'Content-Type': 'application/json' })) - .it('succeds creating a workspace from a valid devfile', async () => { - const res = await ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.valid', undefined) - expect(res.links!.ide).to.equal('https://che-che.192.168.64.39.nip.io/che/chectl') - }) - fancy - .stub(ch, 'cheNamespaceExist', () => true) - .stub(ch, 'cheURL', () => cheURL) - .nock(cheURL, api => api - .post(devfileEndpoint) - .replyWithFile(400, __dirname + '/replies/create-workspace-from-invalid-devfile.json', { - 'Content-Type': 'application/json' - })) - .do(() => ch.createWorkspaceFromDevfile(namespace, __dirname + '/requests/devfile.invalid', undefined)) - .catch(/E_BAD_DEVFILE_FORMAT/) - .it('fails creating a workspace from an invalid devfile') fancy .stub(ch, 'cheNamespaceExist', () => true) .stub(ch, 'cheURL', () => cheURL) @@ -171,7 +100,7 @@ describe('Eclipse Che helper', () => { .post(devfileEndpoint) .replyWithFile(201, __dirname + '/replies/create-workspace-from-valid-devfile.json', { 'Content-Type': 'application/json' })) .it('succeeds creating a workspace from a remote devfile', async () => { - const res = await ch.createWorkspaceFromDevfile(namespace, devfileServerURL + '/devfile.yaml', undefined) + const res = await ch.createWorkspaceFromDevfile(cheURL + '/api', devfileServerURL + '/devfile.yaml') expect(res.links!.ide).to.equal('https://che-che.192.168.64.39.nip.io/che/chectl') }) fancy @@ -208,26 +137,4 @@ describe('Eclipse Che helper', () => { .catch(/Pod is not found for the given workspace ID/) .it('should fail if no workspace is found for the given ID') }) - describe('isAuthenticationEnabled', () => { - fancy - .nock(cheURL, api => api - .get('/api/keycloak/settings') - .replyWithFile(200, __dirname + '/replies/get-keycloak-settings.json', { - 'Content-Type': 'application/json' - })) - .it('should return true if the api/keycloak/settings endpoint doesn\'t exist', async () => { - const authEnabled = await ch.isAuthenticationEnabled(cheURL) - expect(authEnabled).to.equal(true) - }) - fancy - .nock(cheURL, api => api - .get('/api/keycloak/settings') - .reply(404, 'Page does not exist', { - 'Content-Type': 'text/plain' - })) - .it('should return false if the api/keycloak/settings endpoint doesn\'t exist', async () => { - const authEnabled = await ch.isAuthenticationEnabled(cheURL) - expect(authEnabled).to.equal(false) - }) - }) }) diff --git a/test/e2e/util/e2e.ts b/test/e2e/util/e2e.ts index 092e91442..7958b8fb6 100644 --- a/test/e2e/util/e2e.ts +++ b/test/e2e/util/e2e.ts @@ -8,11 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ +import { che as chetypes } from '@eclipse-che/api' import axios, { AxiosInstance } from 'axios' import { Agent } from 'https' import { stringify } from 'querystring' import { CheHelper } from '../../../src/api/che' +import { CheApiClient } from '../../../src/api/che-api-client' import { KubeHelper } from '../../../src/api/kube' import { OpenShiftHelper } from '../../../src/api/openshift' @@ -46,8 +48,8 @@ export class E2eHelper { } try { if (platform === 'openshift') { - const keycloak_url = await this.OCHostname('keycloak') - const endpoint = `${keycloak_url}/auth/realms/che/protocol/openid-connect/token` + const keycloakUrl = await this.OCHostname('keycloak') + const endpoint = `${keycloakUrl}/auth/realms/che/protocol/openid-connect/token` const accessToken = await this.axios.post(endpoint, stringify(params)) return accessToken.data.access_token @@ -64,25 +66,21 @@ export class E2eHelper { } //Return an array with all workspaces - async getAllWorkspaces(isOpenshiftPlatformFamily: string): Promise { - let workspaces = [] - const maxItems = 30 - let skipCount = 0 + async getAllWorkspaces(isOpenshiftPlatformFamily: string): Promise { + let cheApiEndpoint: string if (isOpenshiftPlatformFamily === 'openshift') { - const cheUrl = await this.OCHostname('che') - workspaces = await this.che.doGetWorkspaces(cheUrl, skipCount, maxItems, process.env.CHE_ACCESS_TOKEN) + cheApiEndpoint = await this.OCHostname('che') + '/api' } else { - const cheUrl = await this.K8SHostname('che') - workspaces = await this.che.doGetWorkspaces(cheUrl, skipCount, maxItems, process.env.CHE_ACCESS_TOKEN) + cheApiEndpoint = await this.K8SHostname('che') + '/api' } - return workspaces + return CheApiClient.getInstance(cheApiEndpoint).getAllWorkspaces(process.env.CHE_ACCESS_TOKEN) } // Return an id of test workspaces(e2e-tests. Please look devfile-example.yaml file) async getWorkspaceId(platform: string): Promise { const workspaces = await this.getAllWorkspaces(platform) - const workspaceId = workspaces.filter((wks => wks.devfile.metadata.name === this.devfileName)).map(({ id }) => id)[0] + const workspaceId = workspaces.filter((wks => wks!.devfile!.metadata!.name === this.devfileName)).map(({ id }) => id)[0] if (!workspaceId) { throw Error('Error getting workspaceId') @@ -95,7 +93,7 @@ export class E2eHelper { // Return the status of test workspaces(e2e-tests. Please look devfile-example.yaml file) async getWorkspaceStatus(platform: string): Promise { const workspaces = await this.getAllWorkspaces(platform) - const workspaceStatus = workspaces.filter((wks => wks.devfile.metadata.name === this.devfileName)).map(({ status }) => status)[0] + const workspaceStatus = workspaces.filter((wks => wks!.devfile!.metadata!.name === this.devfileName)).map(({ status }) => status)[0] if (!workspaceStatus) { throw Error('Error getting workspace_id') @@ -106,35 +104,30 @@ export class E2eHelper { } //Return a route from Openshift adding protocol - async OCHostname(ingressName: string): Promise { + async OCHostname(ingressName: string): Promise { if (await this.oc.routeExist(ingressName, 'che')) { - try { - const protocol = await this.oc.getRouteProtocol(ingressName, 'che') - const hostname = await this.oc.getRouteHost(ingressName, 'che') + const protocol = await this.oc.getRouteProtocol(ingressName, 'che') + const hostname = await this.oc.getRouteHost(ingressName, 'che') - return `${protocol}://${hostname}` - } catch (error) { - return error - } + return `${protocol}://${hostname}` } + throw new Error('Route "che" does not exist') } // Return ingress and protocol from minikube platform - async K8SHostname(ingressName: string): Promise { + async K8SHostname(ingressName: string): Promise { if (await this.kubeHelper.ingressExist(ingressName, 'che')) { - try { - const protocol = await this.kubeHelper.getIngressProtocol(ingressName, 'che') - const hostname = await this.kubeHelper.getIngressHost(ingressName, 'che') + const protocol = await this.kubeHelper.getIngressProtocol(ingressName, 'che') + const hostname = await this.kubeHelper.getIngressHost(ingressName, 'che') - return `${protocol}://${hostname}` - } catch (error) { - return error - } + return `${protocol}://${hostname}` } + throw new Error('Ingress "che" does not exist') } // Utility to wait a time SleepTests(ms: number): Promise { + // tslint:disable-next-line no-string-based-set-timeout return new Promise(resolve => setTimeout(resolve, ms)) } }