From df3760f16a5d24896078a445475e7d0eda431e90 Mon Sep 17 00:00:00 2001 From: Flavius Lacatusu Date: Tue, 8 Jun 2021 13:48:59 +0200 Subject: [PATCH] fixes Signed-off-by: Flavius Lacatusu --- configs/disabled.rules.chectl.json | 5 +- configs/eslint.license.json | 4 +- src/api/che-api-client.ts | 694 +-- src/api/che-login-manager.ts | 21 +- src/api/che.ts | 11 +- src/api/config-manager.ts | 2 +- src/api/context.ts | 2 +- src/api/devfile.ts | 2 +- src/api/github-client.ts | 40 +- src/api/kube.ts | 5225 +++++++++-------- src/api/openshift.ts | 5 +- src/api/typings/cert-manager.d.ts | 2 +- src/api/typings/olm.d.ts | 2 +- src/api/typings/openshift.d.ts | 2 +- src/api/version.ts | 4 +- src/commands/auth/delete.ts | 2 +- src/commands/auth/get.ts | 2 +- src/commands/auth/list.ts | 2 +- src/commands/auth/login.ts | 2 +- src/commands/auth/logout.ts | 2 +- src/commands/auth/use.ts | 4 +- src/commands/cacert/export.ts | 2 +- src/commands/dashboard/open.ts | 2 +- src/commands/devfile/generate.ts | 5 +- src/commands/server/debug.ts | 2 +- src/commands/server/delete.ts | 2 +- src/commands/server/deploy.ts | 888 +-- src/commands/server/logs.ts | 2 +- src/commands/server/start.ts | 106 +- src/commands/server/status.ts | 2 +- src/commands/server/stop.ts | 2 +- src/commands/server/update.ts | 7 +- src/commands/workspace/create.ts | 2 +- src/commands/workspace/delete.ts | 2 +- src/commands/workspace/inject.ts | 11 +- src/commands/workspace/list.ts | 2 +- src/commands/workspace/logs.ts | 2 +- src/commands/workspace/start.ts | 2 +- src/commands/workspace/stop.ts | 2 +- src/common-flags.ts | 2 +- src/constants.ts | 2 +- src/hooks/analytics/analytics.ts | 2 +- src/hooks/analytics/segment-adapter.ts | 2 +- src/hooks/prerun/new-version-warning.ts | 2 +- src/index.ts | 2 +- src/tasks/che.ts | 2 +- .../component-installers/cert-manager.ts | 2 +- .../devfile-workspace-operator-installer.ts | 2 +- src/tasks/installers/common-tasks.ts | 2 +- src/tasks/installers/helm.ts | 22 +- src/tasks/installers/installer.ts | 2 +- src/tasks/installers/olm.ts | 5 +- src/tasks/installers/operator.ts | 2 +- src/tasks/kube.ts | 2 +- src/tasks/platforms/api.ts | 10 +- src/tasks/platforms/common-platform-tasks.ts | 2 +- src/tasks/platforms/crc.ts | 5 +- src/tasks/platforms/docker-desktop.ts | 2 +- src/tasks/platforms/k8s.ts | 2 +- src/tasks/platforms/microk8s.ts | 6 +- src/tasks/platforms/minikube.ts | 7 +- src/tasks/platforms/minishift.ts | 5 +- src/tasks/platforms/openshift.ts | 2 +- src/tasks/platforms/platform.ts | 2 +- src/util.ts | 5 +- 65 files changed, 3614 insertions(+), 3565 deletions(-) diff --git a/configs/disabled.rules.chectl.json b/configs/disabled.rules.chectl.json index b5c8ff798..3703e713c 100644 --- a/configs/disabled.rules.chectl.json +++ b/configs/disabled.rules.chectl.json @@ -22,6 +22,9 @@ "no-multi-assign": 0, "no-lonely-if": 0, "no-async-promise-executor": 0, - "prefer-promise-reject-errors": 0 + "prefer-promise-reject-errors": 0, + "no-else-return": 0, + "no-useless-return": 0, + "complexity": 0 } } diff --git a/configs/eslint.license.json b/configs/eslint.license.json index 00f5af1c1..162e76971 100644 --- a/configs/eslint.license.json +++ b/configs/eslint.license.json @@ -7,8 +7,8 @@ [ "*", { - "pattern": "^ \\* Copyright \\(c\\) 2021 Red Hat, Inc\\.$", - "template": " * Copyright (c) 2021 Red Hat, Inc." + "pattern": "^ \\* Copyright \\(c\\) \\d{4}(-\\d{4})* Red Hat, Inc\\.$", + "template": " * Copyright (c) 2019-2021 Red Hat, Inc." }, " * This program and the accompanying materials are made", " * available under the terms of the Eclipse Public License 2.0", diff --git a/src/api/che-api-client.ts b/src/api/che-api-client.ts index 3e19534f6..0d0303942 100644 --- a/src/api/che-api-client.ts +++ b/src/api/che-api-client.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -18,349 +18,355 @@ import * as https from 'https' import { newError, sleep } from '../util' /** - * Singleton responsible for calls to Che API. - */ + * 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 - } - - public static normalizeCheApiEndpointUrl(url: string) { - 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) - } - this.checkResponse(response, endpoint) - 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 - } - 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++) { - const 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 - } - 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) - } - } - - this.checkResponse(response, endpoint) - } - - 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 - } - throw new Error('E_BAD_RESP_CHE_SERVER') - } - - /** - * Returns Keycloak settings or undefined for single user mode. - */ - async getKeycloakSettings(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { - const endpoint = `${this.cheApiEndpoint}/keycloak/settings` - let response - try { - response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) - } catch (error) { - if (error.response && error.response.status === 404) { - return - } - throw this.getCheApiError(error, endpoint) - } - this.checkResponse(response, endpoint) - if (!response.data['che.keycloak.token.endpoint']) { - // The response is not keycloak response, but a default fallback - throw new Error('E_BAD_CHE_API_URL') - } - return response.data - } - - async isAuthenticationEnabled(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { - const endpoint = `${this.cheApiEndpoint}/keycloak/settings` - let response - try { - response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) - } catch (error) { - if (error.response && (error.response.status === 404 || error.response.status === 503)) { - return false - } - throw this.getCheApiError(error, endpoint) - } - this.checkResponse(response, endpoint) - if (!response.data['che.keycloak.token.endpoint']) { - // The response is not keycloak response, but a default fallback - return false - } - return true - } - - private checkResponse(response: any, endpoint?: string): void { - if (!response || response.status !== 200 || !response.data) { - throw new Error(`E_BAD_RESP_CHE_API - Response code: ${response.status}` + endpoint ? `, endpoint: ${endpoint}` : '') - } - } - - private getCheApiError(error: any, endpoint: string): Error { - if (error.response) { - const status = error.response.status - if (status === 403) { - return newError(`E_CHE_API_FORBIDDEN - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data.message)}`, error) - } if (status === 401) { - return newError(`E_CHE_API_UNAUTHORIZED - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`, error) - } if (status === 404) { - return newError(`E_CHE_API_NOTFOUND - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`, error) - } if (status === 503) { - return newError(`E_CHE_API_UNAVAIL - Endpoint: ${endpoint} returned 503 code`, error) - } - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - return newError(`E_CHE_API_UNKNOWN_ERROR - Endpoint: ${endpoint} -Status: ${error.response.status}`, error) - } 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 newError(`E_CHE_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`, error) - } - // Something happened in setting up the request that triggered an Error - return newError(`E_CHECTL_UNKNOWN_ERROR - Endpoint: ${endpoint} - Message: ${error.message}`, error) - } + 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 + } + + public static normalizeCheApiEndpointUrl(url: string) { + 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) + } + this.checkResponse(response, endpoint) + 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++) { + const 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) + } + } + + this.checkResponse(response, endpoint) + } + + 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') + } + } + + /** + * Returns Keycloak settings or undefined for single user mode. + */ + async getKeycloakSettings(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const endpoint = `${this.cheApiEndpoint}/keycloak/settings` + let response + try { + response = await this.axios.get(endpoint, { timeout: responseTimeoutMs }) + } catch (error) { + if (error.response && error.response.status === 404) { + return + } + throw this.getCheApiError(error, endpoint) + } + this.checkResponse(response, endpoint) + if (!response.data['che.keycloak.token.endpoint']) { + // The response is not keycloak response, but a default fallback + throw new Error('E_BAD_CHE_API_URL') + } + return response.data + } + + async isAuthenticationEnabled(responseTimeoutMs = this.defaultCheResponseTimeoutMs): Promise { + const endpoint = `${this.cheApiEndpoint}/keycloak/settings` + let response + 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) + } + } + this.checkResponse(response, endpoint) + if (!response.data['che.keycloak.token.endpoint']) { + // The response is not keycloak response, but a default fallback + return false + } + return true + } + + private checkResponse(response: any, endpoint?: string): void { + if (!response || response.status !== 200 || !response.data) { + throw new Error(`E_BAD_RESP_CHE_API - Response code: ${response.status}` + endpoint ? `, endpoint: ${endpoint}` : '') + } + } + + private getCheApiError(error: any, endpoint: string): Error { + if (error.response) { + const status = error.response.status + if (status === 403) { + return newError(`E_CHE_API_FORBIDDEN - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data.message)}`, error) + } else if (status === 401) { + return newError(`E_CHE_API_UNAUTHORIZED - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`, error) + } else if (status === 404) { + return newError(`E_CHE_API_NOTFOUND - Endpoint: ${endpoint} - Message: ${JSON.stringify(error.response.data)}`, error) + } else if (status === 503) { + return newError(`E_CHE_API_UNAVAIL - Endpoint: ${endpoint} returned 503 code`, error) + } else { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + return newError(`E_CHE_API_UNKNOWN_ERROR - Endpoint: ${endpoint} -Status: ${error.response.status}`, error) + } + } 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 newError(`E_CHE_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`, error) + } else { + // Something happened in setting up the request that triggered an Error + return newError(`E_CHECTL_UNKNOWN_ERROR - Endpoint: ${endpoint} - Message: ${error.message}`, error) + } + } } diff --git a/src/api/che-login-manager.ts b/src/api/che-login-manager.ts index c355182c6..2ce4c620b 100644 --- a/src/api/che-login-manager.ts +++ b/src/api/che-login-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -172,8 +172,9 @@ export class CheServerLoginManager { apiUrl = CheApiClient.normalizeCheApiEndpointUrl(apiUrl) if (username) { return Boolean(this.getLoginRecord(apiUrl, username)) + } else { + return Boolean(this.loginData.logins![apiUrl]) } - return Boolean(this.loginData.logins![apiUrl]) } public getCurrentLoginInfo(): { cheApiEndpoint: string, username: string } { @@ -432,14 +433,16 @@ export class CheServerLoginManager { } if (isPasswordLoginData(loginRecord)) { return this.getKeycloakAuthDataByUserNameAndPassword(cheKeycloakSettings, loginRecord.username, loginRecord.password) + } else { + if (isRefreshTokenLoginData(loginRecord)) { + return this.getKeycloakAuthDataByRefreshToken(cheKeycloakSettings, loginRecord.refreshToken) + } else if (isOcUserTokenLoginData(loginRecord)) { + return this.getKeycloakAuthDataByOcToken(cheKeycloakSettings, loginRecord.subjectToken, loginRecord.subjectIssuer) + } else { + // Should never happen + throw new Error('Token is not provided') + } } - if (isRefreshTokenLoginData(loginRecord)) { - return this.getKeycloakAuthDataByRefreshToken(cheKeycloakSettings, loginRecord.refreshToken) - } if (isOcUserTokenLoginData(loginRecord)) { - return this.getKeycloakAuthDataByOcToken(cheKeycloakSettings, loginRecord.subjectToken, loginRecord.subjectIssuer) - } - // Should never happen - throw new Error('Token is not provided') } private async getKeycloakAuthDataByUserNameAndPassword(cheKeycloakSettings: CheKeycloakSettings, username: string, password: string): Promise { diff --git a/src/api/che.ts b/src/api/che.ts index 3b2e66ee1..9995b90ff 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -113,8 +113,9 @@ export class CheHelper { if (await this.kube.isOpenShift()) { return this.cheOpenShiftURL(namespace) + } else { + return this.cheK8sURL(namespace) } - return this.cheK8sURL(namespace) } async chePluginRegistryURL(namespace = ''): Promise { @@ -130,8 +131,9 @@ export class CheHelper { // grab URL if (await this.kube.isOpenShift()) { return this.chePluginRegistryOpenShiftURL(namespace) + } else { + return this.chePluginRegistryK8sURL(namespace) } - return this.chePluginRegistryK8sURL(namespace) } async isSelfSignedCertificateSecretExist(namespace: string): Promise { @@ -303,8 +305,9 @@ export class CheHelper { if (devfilePath.startsWith('http')) { const response = await this.axios.get(devfilePath) return response.data + } else { + return fs.readFileSync(devfilePath, 'utf8') } - return fs.readFileSync(devfilePath, 'utf8') } async buildDashboardURL(ideURL: string): Promise { diff --git a/src/api/config-manager.ts b/src/api/config-manager.ts index 4eafefc58..25ce94bf5 100644 --- a/src/api/config-manager.ts +++ b/src/api/config-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/context.ts b/src/api/context.ts index 1a44fe074..a9f70691d 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/devfile.ts b/src/api/devfile.ts index 08b8c3219..a74e9d33c 100644 --- a/src/api/devfile.ts +++ b/src/api/devfile.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/github-client.ts b/src/api/github-client.ts index 47f7a6e2c..9fead1c67 100644 --- a/src/api/github-client.ts +++ b/src/api/github-client.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -42,7 +42,7 @@ export class CheGithubClient { async getTemplatesTagInfo(installer: string, version?: string): Promise { if (installer === 'operator' || installer === 'olm') { return this.getTagInfoByVersion(CHE_OPERATOR_REPO, version) - } if (installer === 'helm') { + } else if (installer === 'helm') { return this.getTagInfoByVersion(CHE_REPO, version) } throw new Error(`Unsupported installer: ${installer}`) @@ -129,21 +129,22 @@ export class CheGithubClient { if (!version || version === 'latest' || version === 'stable') { const tags = await this.listLatestTags(repo) return this.getLatestTag(tags) - } if (version === 'next' || version === 'nightly') { + } else if (version === 'next' || version === 'nightly') { return this.getLastCommitInfo(repo) + } else { + // User might provide a version directly or only version prefix, e.g. 7.15 + // Some old tags might have 'v' prefix + if (version.startsWith('v')) { + // Remove 'v' prefix + version = version.substr(1) + } + let tagInfo = await this.getTagInfoByVersionPrefix(repo, version) + if (!tagInfo) { + // Try to add 'v' prefix + tagInfo = tagInfo = await this.getTagInfoByVersionPrefix(repo, 'v' + version) + } + return tagInfo } - // User might provide a version directly or only version prefix, e.g. 7.15 - // Some old tags might have 'v' prefix - if (version.startsWith('v')) { - // Remove 'v' prefix - version = version.substr(1) - } - let tagInfo = await this.getTagInfoByVersionPrefix(repo, version) - if (!tagInfo) { - // Try to add 'v' prefix - tagInfo = tagInfo = await this.getTagInfoByVersionPrefix(repo, 'v' + version) - } - return tagInfo } /** @@ -162,7 +163,7 @@ export class CheGithubClient { const tags = await this.listLatestTags(repo, versionPrefix) if (tags.length === 0) { // Wrong version is given - + return } else if (tags.length === 1) { return tags[0] } else { @@ -225,12 +226,13 @@ export class CheGithubClient { const sortedSemanticTags = semanticTags.sort((semTagA: SemanticTagData, semTagB: SemanticTagData) => { if (semTagA.major !== semTagB.major) { return semTagB.major - semTagA.major - } if (semTagA.minor !== semTagB.minor) { + } else if (semTagA.minor !== semTagB.minor) { return semTagB.minor - semTagA.minor - } if (semTagA.patch !== semTagB.patch) { + } else if (semTagA.patch !== semTagB.patch) { return semTagB.patch - semTagA.patch + } else { + return 0 } - return 0 }) return sortedSemanticTags.map(tag => tag.data) diff --git a/src/api/kube.ts b/src/api/kube.ts index 90028773f..d1de2f1ca 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -16,7 +16,7 @@ import axios, { AxiosRequestConfig } from 'axios' import { cli } from 'cli-ux' import * as execa from 'execa' import * as fs from 'fs' - import https = require('https') +import https = require('https') import { merge } from 'lodash' import * as net from 'net' import { Writable } from 'stream' @@ -32,2613 +32,2620 @@ import { VersionHelper } from './version' const AWAIT_TIMEOUT_S = 30 export class KubeHelper { - public readonly kubeConfig - - readonly API_EXTENSIONS_V1BETA1 = 'apiextensions.k8s.io/v1beta1' - - podWaitTimeout: number - - podDownloadImageTimeout: number - - podReadyTimeout: number - - podErrorRecheckTimeout: number - - constructor(flags?: any) { - this.podWaitTimeout = (flags && flags.k8spodwaittimeout) ? parseInt(flags.k8spodwaittimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT - this.podReadyTimeout = (flags && flags.k8spodreadytimeout) ? parseInt(flags.k8spodreadytimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT - this.podDownloadImageTimeout = (flags && flags.k8spoddownloadimagetimeout) ? parseInt(flags.k8spoddownloadimagetimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT - this.podErrorRecheckTimeout = (flags && flags.spoderrorrechecktimeout) ? parseInt(flags.spoderrorrechecktimeout, 10) : DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT - this.kubeConfig = new KubeConfig() - this.kubeConfig.loadFromDefault() - } - - async createNamespace(namespaceName: string, labels: any): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - const namespaceObject = { - apiVersion: 'v1', - kind: 'Namespace', - metadata: { - labels, - name: namespaceName, - }, - } - - try { - await k8sCoreApi.createNamespace(namespaceObject) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteAllServices(namespace: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sApi.listNamespacedService(namespace) - if (res && res.response && res.response.statusCode === 200) { - const serviceList = res.body - await serviceList.items.forEach(async service => { - try { - await k8sApi.deleteNamespacedService(service.metadata!.name!, namespace) - } catch (error) { - if (error.response.statusCode !== 404) { - throw error - } - } - }) - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async applyResource(yamlPath: string, opts = ''): Promise { - const command = `kubectl apply -f ${yamlPath} ${opts}` - await execa(command, { timeout: 30000, shell: true }) - } - - async getServicesBySelector(labelSelector = '', namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sCoreApi.listNamespacedService(namespace, 'true', undefined, undefined, undefined, labelSelector) - if (res && res.body) { - return res.body - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - throw new Error('ERR_LIST_SERVICES') - } - - async waitForService(selector: string, namespace = '', intervalMs = 500, timeoutMs = 30000) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - const currentServices = await this.getServicesBySelector(selector, namespace) - if (currentServices && currentServices.items.length > 0) { - return - } - await cli.wait(intervalMs) - } - throw new Error(`ERR_TIMEOUT: Timeout set to waiting for service ${timeoutMs}`) - } - - async serviceAccountExist(name = '', namespace = ''): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body } = await k8sApi.readNamespacedServiceAccount(name, namespace) - return this.compare(body, name) - } catch { - return false - } - } - - async createServiceAccount(name = '', namespace = '') { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - const sa = new V1ServiceAccount() - sa.metadata = new V1ObjectMeta() - sa.metadata.name = name - sa.metadata.namespace = namespace - try { - return await k8sCoreApi.createNamespacedServiceAccount(namespace, sa) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async waitServiceAccount(name: string, namespace: string, timeout = AWAIT_TIMEOUT_S): Promise { - return new Promise(async (resolve, reject) => { - // Set up watcher - const watcher = new Watch(this.kubeConfig) - const request = await watcher - .watch(`/api/v1/namespaces/${namespace}/serviceaccounts`, {}, - (_phase: string, obj: any) => { - const serviceAccount = obj as V1ServiceAccount - - // Filter other service accounts in the given namespace - if (serviceAccount && serviceAccount.metadata && serviceAccount.metadata.name === name) { - // The service account is present, stop watching - if (request) { - request.abort() - } - // Release awaiter - resolve() - } - }, - error => { - if (error) { - reject(error) - } - }) - - // Automatically stop watching after timeout - const timeoutHandler = setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${name}" service account.`) - }, timeout * 1000) - - // Request service account, for case if it is already exist - const serviceAccount = await this.getSecret(name, namespace) - if (serviceAccount) { - // Stop watching - request.abort() - clearTimeout(timeoutHandler) - - // Relese awaiter - resolve() - } - }) - } - - async deleteServiceAccount(name: string, namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - await k8sCoreApi.deleteNamespacedServiceAccount(name, namespace) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async createServiceAccountFromFile(filePath: string, namespace = '') { - const yamlServiceAccount = this.safeLoadFromYamlFile(filePath) as V1ServiceAccount - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - return await k8sCoreApi.createNamespacedServiceAccount(namespace, yamlServiceAccount) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceServiceAccountFromFile(filePath: string, namespace = '') { - const yamlServiceAccount = this.safeLoadFromYamlFile(filePath) as V1ServiceAccount - if (!yamlServiceAccount || !yamlServiceAccount.metadata || !yamlServiceAccount.metadata.name) { - throw new Error(`Service account read from ${filePath} must have name specified.`) - } - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - return await k8sCoreApi.replaceNamespacedServiceAccount(yamlServiceAccount.metadata.name, namespace, yamlServiceAccount) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async roleExist(name = '', namespace = ''): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const { body } = await k8sRbacAuthApi.readNamespacedRole(name, namespace) - return this.compare(body, name) - } catch { - return false - } - } - - async clusterRoleExist(name = ''): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const { body } = await k8sRbacAuthApi.readClusterRole(name) - return this.compare(body, name) - } catch { - return false - } - } - - async getClusterRole(name: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const { body } = await k8sRbacAuthApi.readClusterRole(name) - return body - } catch { - - } - } - - async getRole(name: string, namespace: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.readNamespacedRole(name, namespace) - return res.body - } catch (e) { - if (e.statusCode === 404) { - return - } - throw this.wrapK8sClientError(e) - } - } - - async listRoles(namespace: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.listNamespacedRole(namespace) - return res.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createRoleFrom(yamlRole: V1Role, namespace: string) { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.createNamespacedRole(namespace, yamlRole) - return res.response.statusCode - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createRoleFromFile(filePath: string, namespace: string) { - const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role - return this.createRoleFrom(yamlRole, namespace) - } - - async replaceRoleFrom(yamlRole: V1Role, namespace: string) { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - - if (!yamlRole.metadata || !yamlRole.metadata.name) { - throw new Error('Role object requires name') - } - try { - const res = await k8sRbacAuthApi.replaceNamespacedRole(yamlRole.metadata.name, namespace, yamlRole) - return res.response.statusCode - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceRoleFromFile(filePath: string, namespace: string) { - const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role - return this.replaceRoleFrom(yamlRole, namespace) - } - - async listClusterRoles(): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.listClusterRole() - return res.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - if (!yamlClusterRole.metadata) { - yamlClusterRole.metadata = {} - } - - if (clusterRoleName) { - yamlClusterRole.metadata.name = clusterRoleName - } else if (!yamlClusterRole.metadata.name) { - throw new Error('Role name is not specified') - } - try { - const res = await k8sRbacAuthApi.createClusterRole(yamlClusterRole) - return res.response.statusCode - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createClusterRoleFromFile(filePath: string, clusterRoleName?: string) { - const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole - return this.createClusterRoleFrom(yamlClusterRole, clusterRoleName) - } - - async replaceClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - if (!yamlClusterRole.metadata) { - yamlClusterRole.metadata = {} - } - - if (clusterRoleName) { - yamlClusterRole.metadata.name = clusterRoleName - } else if (!yamlClusterRole.metadata.name) { - throw new Error('Role name is not specified') - } - try { - const res = await k8sRbacAuthApi.replaceClusterRole(yamlClusterRole.metadata.name, yamlClusterRole) - return res.response.statusCode - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceClusterRoleFromFile(filePath: string, clusterRoleName?: string) { - const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole - return this.replaceClusterRoleFrom(yamlClusterRole, clusterRoleName) - } - - async addClusterRoleRule(name: string, apiGroups: string[], resources: string[], verbs: string[]): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - const clusterRole = await this.getClusterRole(name) - if (clusterRole) { - // Clean up metadata, otherwise replace role call will fail - clusterRole.metadata = {} - clusterRole.metadata.name = name - - // Add new policy - const additionaRule = new V1PolicyRule() - additionaRule.apiGroups = apiGroups - additionaRule.resources = resources - additionaRule.verbs = verbs - if (clusterRole.rules) { - clusterRole.rules.push(additionaRule) - } - - try { - const { body } = await k8sRbacAuthApi.replaceClusterRole(name, clusterRole) - return body - } catch { - - } - } - } - - async deleteRole(name: string, namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - await k8sCoreApi.deleteNamespacedRole(name, namespace) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async getPodListByLabel(namespace: string, labelSelector: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body: podList } = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, labelSelector) - - return podList.items - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteClusterRole(name: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - await k8sCoreApi.deleteClusterRole(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async listRoleBindings(namespace: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.listNamespacedRoleBinding(namespace) - return res.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async roleBindingExist(name = '', namespace = ''): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - await k8sRbacAuthApi.readNamespacedRoleBinding(name, namespace) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async isMutatingWebhookConfigurationExists(name: string): Promise { - const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) - try { - await k8sAdmissionApi.readMutatingWebhookConfiguration(name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async getMutatingWebhookConfiguration(name: string): Promise { - const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) - try { - const res = await k8sAdmissionApi.readMutatingWebhookConfiguration(name) - return res.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async isValidatingWebhookConfigurationExists(name: string): Promise { - const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) - try { - await k8sAdmissionApi.readValidatingWebhookConfiguration(name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async deleteValidatingWebhookConfiguration(name: string): Promise { - const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) - try { - await k8sAdmissionApi.deleteValidatingWebhookConfiguration(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async deleteMutatingWebhookConfiguration(name: string): Promise { - const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) - try { - await k8sAdmissionApi.deleteMutatingWebhookConfiguration(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async listClusterRoleBindings(labelSelector?: string, fieldSelector?: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const res = await k8sRbacAuthApi.listClusterRoleBinding(undefined, undefined, undefined, fieldSelector, labelSelector) - return res.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async clusterRoleBindingExist(name: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const { body } = await k8sRbacAuthApi.readClusterRoleBinding(name) - return this.compare(body, name) - } catch { - return false - } - } - - async createAdminRoleBinding(name = '', serviceAccount = '', namespace = '') { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - const rb = new V1RoleBinding() - rb.metadata = new V1ObjectMeta() - rb.metadata.name = name - rb.metadata.namespace = namespace - rb.roleRef = new V1RoleRef() - rb.roleRef.kind = 'ClusterRole' - rb.roleRef.name = 'admin' - const subject = new V1Subject() - subject.kind = 'ServiceAccount' - subject.name = serviceAccount - subject.namespace = namespace - rb.subjects = [subject] - try { - return await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, rb) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const response = await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, yamlRoleBinding) - return response.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createRoleBindingFromFile(filePath: string, namespace: string): Promise { - const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding - return this.createRoleBindingFrom(yamlRoleBinding, namespace) - } - - async replaceRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { - if (!yamlRoleBinding.metadata || !yamlRoleBinding.metadata.name) { - throw new Error('RoleBinding object requires name') - } - - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - const response = await k8sRbacAuthApi.replaceNamespacedRoleBinding(yamlRoleBinding.metadata.name, namespace, yamlRoleBinding) - return response.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceRoleBindingFromFile(filePath: string, namespace: string): Promise { - const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding - return this.replaceRoleBindingFrom(yamlRoleBinding, namespace) - } - - async createClusterRoleBindingFrom(yamlClusterRoleBinding: V1ClusterRoleBinding) { - if (!yamlClusterRoleBinding.metadata || !yamlClusterRoleBinding.metadata.name) { - throw new Error('ClusterRoleBinding object requires name') - } - - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - return await k8sRbacAuthApi.createClusterRoleBinding(yamlClusterRoleBinding) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createClusterRoleBinding(name: string, saName: string, saNamespace = '', roleName = '') { - const clusterRoleBinding = { - apiVersion: 'rbac.authorization.k8s.io/v1', - metadata: { - name: `${name}`, - }, - subjects: [ - { - kind: 'ServiceAccount', - name: `${saName}`, - namespace: `${saNamespace}`, - }, - ], - roleRef: { - kind: 'ClusterRole', - name: `${roleName}`, - apiGroup: 'rbac.authorization.k8s.io', - }, - } as V1ClusterRoleBinding - return this.createClusterRoleBindingFrom(clusterRoleBinding) - } - - async replaceClusterRoleBindingFrom(clusterRoleBinding: V1ClusterRoleBinding) { - if (!clusterRoleBinding.metadata || !clusterRoleBinding.metadata.name) { - throw new Error('Cluster Role Binding must have name specified') - } - - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - return await k8sRbacAuthApi.replaceClusterRoleBinding(clusterRoleBinding.metadata.name, clusterRoleBinding) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceClusterRoleBinding(name: string, saName: string, saNamespace = '', roleName = '') { - const clusterRoleBinding = { - apiVersion: 'rbac.authorization.k8s.io/v1', - metadata: { - name: `${name}`, - }, - subjects: [ - { - kind: 'ServiceAccount', - name: `${saName}`, - namespace: `${saNamespace}`, - }, - ], - roleRef: { - kind: 'ClusterRole', - name: `${roleName}`, - apiGroup: 'rbac.authorization.k8s.io', - }, - } as V1ClusterRoleBinding - return this.replaceClusterRoleBindingFrom(clusterRoleBinding) - } - - async deleteRoleBinding(name: string, namespace: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - await k8sRbacAuthApi.deleteNamespacedRoleBinding(name, namespace) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async deleteClusterRoleBinding(name: string): Promise { - const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) - try { - await k8sRbacAuthApi.deleteClusterRoleBinding(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async getConfigMap(name = '', namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body } = await k8sCoreApi.readNamespacedConfigMap(name, namespace) - return this.compare(body, name) && body - } catch { - - } - } - - async getConfigMapValue(name: string, namespace: string, key: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body } = await k8sCoreApi.readNamespacedConfigMap(name, namespace) - if (body.data) { - return body.data[key] - } - } catch { - - } - } - - async createConfigMapFromFile(filePath: string, namespace = '') { - const yamlConfigMap = this.safeLoadFromYamlFile(filePath) as V1ConfigMap - return this.createNamespacedConfigMap(namespace, yamlConfigMap) - } - - public async createNamespacedConfigMap(namespace: string, configMap: V1ConfigMap) { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - - try { - const { body } = await k8sCoreApi.createNamespacedConfigMap(namespace, configMap) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async patchConfigMap(name: string, patch: any, namespace = '') { - const k8sCoreApi = this.kubeConfig.makeApiClient(PatchedK8sApi) - try { - return await k8sCoreApi.patchNamespacedConfigMap(name, namespace, patch) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteConfigMap(name: string, namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - await k8sCoreApi.deleteNamespacedConfigMap(name, namespace) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - public async replaceNamespacedConfigMap(name: string, namespace: string, configMap: V1ConfigMap) { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - - try { - const { body } = await k8sCoreApi.replaceNamespacedConfigMap(name, namespace, configMap) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getNamespace(namespace: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body } = await k8sApi.readNamespace(namespace) - return body - } catch { - } - } - - async hasReadPermissionsForNamespace(namespace: string): Promise { - const k8sApi = this.kubeConfig.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 = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sCoreApi.readNamespacedPod(podName, namespace) - if (res && res.body) { - return res.body - } - } catch { - - } - } - - async patchCustomResource(name: string, namespace: string, patch: any, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - // It is required to patch content-type, otherwise request will be rejected with 415 (Unsupported media type) error. - const requestOptions = { - headers: { - 'content-type': 'application/merge-patch+json', - }, - } - - try { - const res = await k8sCoreApi.patchNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural, name, patch, undefined, undefined, undefined, requestOptions) - if (res && res.body) { - return res.body - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async patchNamespacedPod(name: string, namespace: string, patch: any): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - - // It is required to patch content-type, otherwise request will be rejected with 415 (Unsupported media type) error. - const requestOptions = { - headers: { - 'content-type': 'application/strategic-merge-patch+json', - }, - } - - try { - const res = await k8sCoreApi.patchNamespacedPod(name, namespace, patch, undefined, undefined, undefined, undefined, requestOptions) - if (res && res.body) { - return res.body - } - } catch { - - } - } - - async podsExistBySelector(selector: string, namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - let res - try { - res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) - } catch (e) { - throw this.wrapK8sClientError(e) - } - - if (!res || !res.body || !res.body.items) { - throw new Error(`Get pods by selector "${selector}" returned an invalid response`) - } - - return (res.body.items.length > 0) - } - - /** - * Returns pod waiting state. - */ - async getPodWaitingState(namespace: string, selector: string, desiredPhase: string): Promise { - const pods = await this.getPodListByLabel(namespace, selector) - if (!pods.length) { - return - } - - for (const pod of pods) { - if (pod.status && pod.status.phase === desiredPhase && pod.status.containerStatuses) { - for (const status of pod.status.containerStatuses) { - if (status.state && status.state.waiting && status.state.waiting.message && status.state.waiting.reason) { - return status.state.waiting - } - } - } - } - } - - /** - * Returns pod last terminated state. - */ - async getPodLastTerminatedState(namespace: string, selector: string): Promise { - const pods = await this.getPodListByLabel(namespace, selector) - if (!pods.length) { - return - } - - for (const pod of pods) { - if (pod.status && pod.status.containerStatuses) { - for (const status of pod.status.containerStatuses) { - if (status.lastState) { - return status.lastState.terminated - } - } - } - } - } - - async getPodCondition(namespace: string, selector: string, conditionType: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - let res - try { - res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) - } catch (e) { - throw this.wrapK8sClientError(e) - } - - if (!res || !res.body || !res.body.items) { - return [] - } - - const conditions: V1PodCondition[] = [] - for (const pod of res.body.items) { - if (pod.status && pod.status.conditions) { - for (const condition of pod.status.conditions) { - if (condition.type === conditionType) { - conditions.push(condition) - } - } - } - } - - return conditions - } - - async getPodReadyConditionStatus(selector: string, namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - let res - try { - res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) - } catch (e) { - throw this.wrapK8sClientError(e) - } - - if (!res || !res.body || !res.body.items) { - throw new Error(`Get pods by selector "${selector}" returned an invalid response.`) - } - - if (res.body.items.length < 1) { - // No pods found by the specified selector. So, it's not ready. - return 'False' - } - - if (res.body.items.length > 1) { - // Several pods found, rolling update? - return - } - - if (!res.body.items[0].status || !res.body.items[0].status.conditions || !(res.body.items[0].status.conditions.length > 0)) { - return - } - - const conditions = res.body.items[0].status.conditions - for (const condition of conditions) { - if (condition.type === 'Ready') { - return condition.status - } - } - } - - async waitForPodReady(selector: string, namespace = '', intervalMs = 500, timeoutMs = this.podReadyTimeout) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - const readyStatus = await this.getPodReadyConditionStatus(selector, namespace) - if (readyStatus === 'True') { - return - } - await cli.wait(intervalMs) - } - throw new Error(`ERR_TIMEOUT: Timeout set to pod ready timeout ${this.podReadyTimeout}`) - } - - async waitUntilPodIsDeleted(selector: string, namespace = '', intervalMs = 500, timeoutMs = this.podReadyTimeout) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - const pods = await this.listNamespacedPod(namespace, undefined, selector) - if (!pods.items.length) { - return - } - await cli.wait(intervalMs) - } - throw new Error('ERR_TIMEOUT: Waiting until pod is deleted took too long.') - } - - async deletePod(name: string, namespace = '') { - this.kubeConfig.loadFromDefault() - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - return await k8sCoreApi.deleteNamespacedPod(name, namespace) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - // make sure that flag is specified for command that it's invoked - async waitLatestReplica(deploymentName: string, namespace = '', intervalMs = 500, timeoutMs = this.podWaitTimeout) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - const deployment = await this.getDeployment(deploymentName, namespace) - if (!deployment) { - throw new Error(`Deployment ${namespace}/${deploymentName} is not found.`) - } - - const deploymentStatus = deployment.status - if (!deploymentStatus) { - throw new Error(`Deployment ${namespace}/${deploymentName} does not have any status`) - } - - if (deploymentStatus.unavailableReplicas && deploymentStatus.unavailableReplicas > 0) { - await cli.wait(intervalMs) - } else { - return - } - } - - throw new Error(`ERR_TIMEOUT: Timeout set to pod wait timeout ${this.podWaitTimeout}`) - } - - async deploymentExist(name = '', namespace = ''): Promise { - const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const { body } = await k8sApi.readNamespacedDeployment(name, namespace) - return this.compare(body, name) - } catch { - return false - } - } - - async isConfigMapExists(name: string, namespace: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - await k8sApi.readNamespacedConfigMap(name, namespace) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async deploymentReady(name = '', namespace = ''): Promise { - const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const res = await k8sApi.readNamespacedDeployment(name, namespace) - return ((res && res.body && - res.body.status && res.body.status.readyReplicas && - res.body.status.readyReplicas > 0) as boolean) - } catch { - return false - } - } - - async deploymentStopped(name = '', namespace = ''): Promise { - const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const res = await k8sApi.readNamespacedDeployment(name, namespace) - if (res && res.body && res.body.spec && res.body.spec.replicas) { - throw new Error(`Deployment '${name}' without replicas in spec is fetched`) - } - return res.body!.spec!.replicas === 0 - } catch { - return false - } - } - - async isDeploymentPaused(name = '', namespace = ''): Promise { - const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const res = await k8sApi.readNamespacedDeployment(name, namespace) - if (!res || !res.body || !res.body.spec) { - throw new Error('E_BAD_DEPLOY_RESPONSE') - } - return res.body.spec.paused || false - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async pauseDeployment(name = '', namespace = '') { - const k8sApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) - try { - const patch = { - spec: { - paused: true, - }, - } - await k8sApi.patchNamespacedDeployment(name, namespace, patch) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async resumeDeployment(name = '', namespace = '') { - const k8sApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) - try { - const patch = { - spec: { - paused: false, - }, - } - await k8sApi.patchNamespacedDeployment(name, namespace, patch) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async scaleDeployment(name = '', namespace = '', replicas: number) { - const k8sAppsApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) - const patch = { - spec: { - replicas, - }, - } - let res - try { - res = await k8sAppsApi.patchNamespacedDeploymentScale(name, namespace, patch) - } catch (e) { - throw this.wrapK8sClientError(e) - } - - if (!res || !res.body) { - throw new Error('Patch deployment scale returned an invalid response') - } - } - - async createDeployment(name: string, - image: string, - serviceAccount: string, - pullPolicy: string, - configMapEnvSource: string, - namespace: string) { - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - const deployment = new V1Deployment() - deployment.metadata = new V1ObjectMeta() - deployment.metadata.name = name - deployment.metadata.namespace = namespace - deployment.spec = new V1DeploymentSpec() - deployment.spec.selector = new V1LabelSelector() - deployment.spec.selector.matchLabels = { app: name } - deployment.spec.template = new V1PodTemplateSpec() - deployment.spec.template.metadata = new V1ObjectMeta() - deployment.spec.template.metadata.name = name - deployment.spec.template.metadata.labels = { app: name } - deployment.spec.template.spec = new V1PodSpec() - deployment.spec.template.spec.serviceAccountName = serviceAccount - const opContainer = new V1Container() - opContainer.name = name - opContainer.image = image - opContainer.imagePullPolicy = pullPolicy - const envFromSource = new V1EnvFromSource() - envFromSource.configMapRef = new V1ConfigMapEnvSource() - envFromSource.configMapRef.name = configMapEnvSource - opContainer.envFrom = [envFromSource] - deployment.spec.template.spec.containers = [opContainer] - - try { - return await k8sAppsApi.createNamespacedDeployment(namespace, deployment) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createDeploymentFrom(yamlDeployment: V1Deployment): Promise { - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - await k8sAppsApi.createNamespacedDeployment(yamlDeployment.metadata!.namespace!, yamlDeployment) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createServiceFrom(yamlService: V1Service, namespace = '') { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - return await k8sApi.createNamespacedService(namespace, yamlService) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceDeploymentFrom(yamlDeployment: V1Deployment): Promise { - // updating restartedAt to make sure that rollout will be restarted - let annotations = yamlDeployment.spec!.template!.metadata!.annotations - if (!annotations) { - annotations = {} - yamlDeployment.spec!.template!.metadata!.annotations = annotations - } - annotations['kubectl.kubernetes.io/restartedAt'] = new Date().toISOString() - - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - await k8sAppsApi.replaceNamespacedDeployment(yamlDeployment.metadata!.name!, yamlDeployment.metadata!.namespace!, yamlDeployment) - } catch (e) { - if (e.response && e.response.body && e.response.body.message && e.response.body.message.toString().endsWith('field is immutable')) { - try { - await k8sAppsApi.deleteNamespacedDeployment(yamlDeployment.metadata!.name!, yamlDeployment.metadata!.namespace!) - await k8sAppsApi.createNamespacedDeployment(yamlDeployment.metadata!.namespace!, yamlDeployment) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - throw this.wrapK8sClientError(e) - } - } - - async deleteAllDeployments(namespace: string): Promise { - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - await k8sAppsApi.deleteCollectionNamespacedDeployment(namespace) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getDeploymentsBySelector(labelSelector = '', namespace = ''): Promise { - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const res = await k8sAppsApi.listNamespacedDeployment(namespace, 'true', undefined, undefined, undefined, labelSelector) - if (res && res.body) { - return res.body - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - throw new Error('ERR_LIST_NAMESPACES') - } - - async getDeployment(name: string, namespace: string): Promise { - const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) - try { - const res = await k8sAppsApi.readNamespacedDeployment(name, namespace) - if (res && res.body) { - return res.body! - } - } catch (error) { - if (error.response && error.response.statusCode === 404) { - return - } - throw this.wrapK8sClientError(error) - } - throw new Error('ERR_GET_DEPLOYMENT') - } - - async createPod(name: string, - image: string, - serviceAccount: string, - restartPolicy: string, - pullPolicy: string, - configMapEnvSource: string, - namespace: string) { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - const pod = new V1Pod() - pod.metadata = new V1ObjectMeta() - pod.metadata.name = name - pod.metadata.labels = { app: name } - pod.metadata.namespace = namespace - pod.spec = new V1PodSpec() - pod.spec.restartPolicy = restartPolicy - pod.spec.serviceAccountName = serviceAccount - const opContainer = new V1Container() - opContainer.name = name - opContainer.image = image - opContainer.imagePullPolicy = pullPolicy - const envFromSource = new V1EnvFromSource() - envFromSource.configMapRef = new V1ConfigMapEnvSource() - envFromSource.configMapRef.name = configMapEnvSource - opContainer.envFrom = [envFromSource] - pod.spec.containers = [opContainer] - - try { - return await k8sCoreApi.createNamespacedPod(namespace, pod) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createJob(name: string, - image: string, - serviceAccount: string, - namespace: string, - backoffLimit = 0, - restartPolicy = 'Never') { - const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) - - const job = new V1Job() - job.metadata = new V1ObjectMeta() - job.metadata.name = name - job.metadata.labels = { app: name } - job.metadata.namespace = namespace - job.spec = new V1JobSpec() - job.spec.ttlSecondsAfterFinished = 10 - job.spec.backoffLimit = backoffLimit - job.spec.template = new V1PodTemplateSpec() - job.spec.template.spec = new V1PodSpec() - job.spec.template.spec.serviceAccountName = serviceAccount - const jobContainer = new V1Container() - jobContainer.name = name - jobContainer.image = image - job.spec.template.spec.restartPolicy = restartPolicy - job.spec.template.spec.containers = [jobContainer] - - try { - return await k8sBatchApi.createNamespacedJob(namespace, job) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getJob(jobName: string, namespace: string): Promise { - const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) - - try { - const result = await k8sBatchApi.readNamespacedJob(jobName, namespace) - return result.body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async waitJob(jobName: string, namespace: string, timeout = AWAIT_TIMEOUT_S): Promise { - return new Promise(async (resolve, reject) => { - // Set up watcher - const watcher = new Watch(this.kubeConfig) - const request = await watcher - .watch(`/apis/batch/v1/namespaces/${namespace}/jobs/`, {}, - (_phase: string, obj: any) => { - const job = obj as V1Job - - // Filter other jobs in the given namespace - if (job && job.metadata && job.metadata.name === jobName) { - // Check job status - if (job.status && job.status.succeeded && job.status.succeeded >= 1) { - // Job is finished, stop watching - if (request) { - request.abort() - } - // Release awaiter - resolve() - } - } - }, - error => { - if (error) { - reject(error) - } - }) - - // Automatically stop watching after timeout - const timeoutHandler = setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${jobName}" job.`) - }, timeout * 1000) - - // Request job, for case if it is already ready - const job = await this.getJob(jobName, namespace) - if (job.status && job.status.succeeded && job.status.succeeded >= 1) { - // Stop watching - request.abort() - clearTimeout(timeoutHandler) - - // Relese awaiter - resolve() - } - }) - } - - async deleteJob(jobName: string, namespace: string): Promise { - const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) - - try { - const result = await k8sBatchApi.deleteNamespacedJob(jobName, namespace) - return result.body.status === 'Success' - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async compare(body: any, name: string): Promise { - if (body && body.metadata && body.metadata.name && body.metadata.name === name) { - return true - } - return false - } - - async ingressExist(name = '', namespace = ''): Promise { - const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) - try { - const { body } = await k8sExtensionsApi.readNamespacedIngress(name, namespace) - return this.compare(body, name) - } catch { - return false - } - } - - async deleteAllIngresses(namespace: string): Promise { - const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) - try { - await k8sExtensionsApi.deleteCollectionNamespacedIngress(namespace) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createCrdFromFile(filePath: string): Promise { - const yaml = this.safeLoadFromYamlFile(filePath) - if (yaml.apiVersion === this.API_EXTENSIONS_V1BETA1) { - return this.createCrdV1Beta1(yaml) - } - return this.createCrdV1(yaml) - } - - private async createCrdV1Beta1(yaml: any): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) - try { - await k8sApi.createCustomResourceDefinition(yaml) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - private async createCrdV1(yaml: any): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) - try { - await k8sApi.createCustomResourceDefinition(yaml) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async replaceCrdFromFile(filePath: string, resourceVersion: string): Promise { - const yaml = this.safeLoadFromYamlFile(filePath) - if (!yaml.metadata || !yaml.metadata.name) { - throw new Error(`Name is not defined in: ${filePath}`) - } - - yaml.metadata.resourceVersion = resourceVersion - if (yaml.apiVersion === this.API_EXTENSIONS_V1BETA1) { - return this.replaceCrdV1Beta1(yaml) - } - return this.replaceCrdV1(yaml) - } - - private async replaceCrdV1Beta1(yaml: any): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) - try { - await k8sApi.replaceCustomResourceDefinition(yaml.metadata.name, yaml) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - private async replaceCrdV1(yaml: any): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) - try { - await k8sApi.replaceCustomResourceDefinition(yaml.metadata.name, yaml) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getCrd(name: string): Promise { - if (await this.IsAPIExtensionSupported('v1')) { - return this.getCrdV1(name) - } - return this.getCrdV1beta1(name) - } - - private async getCrdV1(name: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) - try { - const { body } = await k8sApi.readCustomResourceDefinition(name) - return body - } catch (e) { - if (e.response.statusCode === 404) { - return - } - - throw this.wrapK8sClientError(e) - } - } - - private async getCrdV1beta1(name: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) - try { - const { body } = await k8sApi.readCustomResourceDefinition(name) - return body - } catch (e) { - if (e.response.statusCode === 404) { - return - } - - throw this.wrapK8sClientError(e) - } - } - - async getCrdStorageVersion(name: string): Promise { - const crd = await this.getCrd(name) - if (!crd.spec.versions) { - // Should never happen - return 'v1' - } - - const version = crd.spec.versions.find((v: any) => v.storage) - return version ? version.name : 'v1' - } - - async deleteCrd(name: string): Promise { - if (await this.IsAPIExtensionSupported('v1')) { - return this.deleteCrdV1(name) - } - return this.deleteCrdV1Beta1(name) - } - - private async deleteCrdV1Beta1(name: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) - try { - await k8sApi.deleteCustomResourceDefinition(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - private async deleteCrdV1(name: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) - try { - await k8sApi.deleteCustomResourceDefinition(name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async createCheCluster(cheClusterCR: any, flags: any, ctx: any, useDefaultCR: boolean): Promise { - const cheNamespace = flags.chenamespace - if (useDefaultCR) { - // If CheCluster CR is not explicitly provided, then modify the default example CR - // with values derived from the other parameters - - if (VersionHelper.isDeployingStableVersion(flags)) { - // Use images from operator defaults in case of a stable version - cheClusterCR.spec.server.cheImage = '' - cheClusterCR.spec.server.cheImageTag = '' - cheClusterCR.spec.server.pluginRegistryImage = '' - cheClusterCR.spec.server.devfileRegistryImage = '' - cheClusterCR.spec.auth.identityProviderImage = '' - } - const cheImage = flags.cheimage - if (cheImage) { - const imageAndTag = cheImage.split(':', 2) - cheClusterCR.spec.server.cheImage = imageAndTag[0] - cheClusterCR.spec.server.cheImageTag = imageAndTag.length === 2 ? imageAndTag[1] : 'latest' - } - - if ((flags.installer === 'olm' && !flags['catalog-source-yaml']) || (flags['catalog-source-yaml'] && flags['olm-channel'] === OLM_STABLE_CHANNEL_NAME)) { - // use default image tag for `olm` to install stable Che, because we don't have nightly channel for OLM catalog. - cheClusterCR.spec.server.cheImageTag = '' - } - cheClusterCR.spec.server.cheDebug = flags.debug ? flags.debug.toString() : 'false' - - if (isKubernetesPlatformFamily(flags.platform) || !cheClusterCR.spec.auth.openShiftoAuth) { - cheClusterCR.spec.auth.updateAdminPassword = true - } - - if (!cheClusterCR.spec.k8s) { - cheClusterCR.spec.k8s = {} - } - if (flags.tls) { - cheClusterCR.spec.server.tlsSupport = flags.tls - if (!cheClusterCR.spec.k8s.tlsSecretName) { - cheClusterCR.spec.k8s.tlsSecretName = 'che-tls' - } - } - if (flags.domain) { - cheClusterCR.spec.k8s.ingressDomain = flags.domain - } - const pluginRegistryUrl = flags['plugin-registry-url'] - if (pluginRegistryUrl) { - cheClusterCR.spec.server.pluginRegistryUrl = pluginRegistryUrl - cheClusterCR.spec.server.externalPluginRegistry = true - } - const devfileRegistryUrl = flags['devfile-registry-url'] - if (devfileRegistryUrl) { - cheClusterCR.spec.server.devfileRegistryUrl = devfileRegistryUrl - cheClusterCR.spec.server.externalDevfileRegistry = true - } - - cheClusterCR.spec.storage.postgresPVCStorageClassName = flags['postgres-pvc-storage-class-name'] - cheClusterCR.spec.storage.workspacePVCStorageClassName = flags['workspace-pvc-storage-class-name'] - - if (flags['workspace-engine'] === 'dev-workspace') { - cheClusterCR.spec.devWorkspace.enable = true - } - - // Use self-signed TLS certificate by default (for versions before 7.14.3). - // In modern versions of Che this field is ignored. - cheClusterCR.spec.server.selfSignedCert = true - } - - cheClusterCR.spec.server.cheClusterRoles = ctx.namespaceEditorClusterRoleName - - // override default values - if (ctx.crPatch) { - merge(cheClusterCR, ctx.crPatch) - } - - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.createNamespacedCustomObject('org.eclipse.che', 'v1', cheNamespace, 'checlusters', cheClusterCR) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async patchCheCluster(name: string, namespace: string, patch: any): Promise { - try { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - const { body } = await customObjectsApi.patchNamespacedCustomObject('org.eclipse.che', 'v1', namespace, 'checlusters', name, patch, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/merge-patch+json' } }) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - /** - * Returns `checlusters.org.eclipse.che' in the given namespace. - */ - async getCheCluster(cheNamespace: string): Promise { - return this.getCustomResource(cheNamespace, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) - } - - /** - * Returns custom resource in the given namespace. - */ - async getCustomResource(namespace: string, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.listNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural) - if (!(body as any).items) { - return - } - - const crs = (body as any).items as any[] - if (crs.length === 0) { - return - } if (crs.length !== 1) { - throw new Error(`Too many resources of type ${resourcePlural}.${resourceAPIGroup} found in the namespace '${namespace}'`) - } - - return crs[0] - } catch (e) { - if (e.response && e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - /** - * Deletes `checlusters.org.eclipse.che' resources in the given namespace. - */ - async getAllCheClusters(): Promise { - return this.getAllCustomResources(CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) - } - - /** - * Returns all custom resources - */ - async getAllCustomResources(resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.listClusterCustomObject(resourceAPIGroup, resourceAPIVersion, resourcePlural) - return (body as any).items ? (body as any).items : [] - } catch (e) { - if (e.response.statusCode === 404) { - // There is no CRD - return [] - } - throw this.wrapK8sClientError(e) - } - } - - /** - * Deletes `checlusters.org.eclipse.che' resources in the given namespace. - */ - async deleteCheCluster(namespace: string): Promise { - return this.deleteCustomResource(namespace, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) - } - - /** - * Deletes custom resources in the given namespace. - */ - async deleteCustomResource(namespace: string, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.listNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural) - if (!(body as any).items) { - return - } - - const crs = (body as any).items as any[] - for (const cr of crs) { - await customObjectsApi.deleteNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural, cr.metadata.name) - } - } catch (e) { - if (e.response.statusCode === 404) { - // There is no CRD - return - } - throw this.wrapK8sClientError(e) - } - } - - async isPreInstalledOLM(): Promise { - const apiApi = this.kubeConfig.makeApiClient(ApisApi) - try { - const { body } = await apiApi.getAPIVersions() - const OLMAPIGroup = body.groups.find(apiGroup => apiGroup.name === 'operators.coreos.com') - return Boolean(OLMAPIGroup) - } catch { - return false - } - } - - async getUsersNumber(): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - let amountOfUsers: number - try { - const { body } = await customObjectsApi.listClusterCustomObject('user.openshift.io', 'v1', 'users') - if (!(body as any).items) { - throw new Error('Unable to get list users.') - } - amountOfUsers = (body as any).items.length - } catch (e) { - throw this.wrapK8sClientError(e) - } - return amountOfUsers - } - - async getOpenshiftAuthProviders(): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - const oAuthName = 'cluster' - const { body } = await customObjectsApi.getClusterCustomObject('config.openshift.io', 'v1', 'oauths', oAuthName) - return (body as OAuth).spec.identityProviders - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async operatorSourceExists(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorsources', name) - return this.compare(body, name) - } catch { - return false - } - } - - async catalogSourceExists(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', name) - return this.compare(body, name) - } catch { - return false - } - } - - async getOAuthClientAuthorizations(clientName: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.listClusterCustomObject('oauth.openshift.io', 'v1', 'oauthclientauthorizations') - - if (!(body as any).items) { - return [] - } - const oauthClientAuthorizations = (body as any).items as any[] - return oauthClientAuthorizations.filter(o => o.clientName === clientName) - } catch (e) { - if (e.response.statusCode === 404) { - // There is no 'oauthclientauthorizations` - return [] - } - throw this.wrapK8sClientError(e) - } - } - - async deleteOAuthClientAuthorizations(oAuthClientAuthorizations: any[]): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const filetOauthAuthorizations = oAuthClientAuthorizations.filter((e => e.metadata && e.metadata.name)) - for (const oauthAuthorization of filetOauthAuthorizations) { - await customObjectsApi.deleteClusterCustomObject('oauth.openshift.io', 'v1', 'oauthclientauthorizations', oauthAuthorization.metadata.name) - } - } catch (e) { - if (e.response.statusCode === 404) { - return - } - throw this.wrapK8sClientError(e) - } - } - - async consoleLinkExists(name: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.getClusterCustomObject('console.openshift.io', 'v1', 'consolelinks', name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - // There are no consoleLink - return false - } - throw this.wrapK8sClientError(e) - } - } - - async deleteConsoleLink(name: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.deleteClusterCustomObject('console.openshift.io', 'v1', 'consolelinks', name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async getCatalogSource(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', name) - return body as CatalogSource - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - readCatalogSourceFromFile(filePath: string): CatalogSource { - const catalogSource = this.safeLoadFromYamlFile(filePath) as CatalogSource - if (!catalogSource.metadata || !catalogSource.metadata.name) { - throw new Error(`CatalogSource from ${filePath} must have specified metadata and name`) - } - return catalogSource - } - - async createCatalogSource(catalogSource: CatalogSource) { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const namespace = catalogSource.metadata.namespace! - const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', catalogSource) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async waitCatalogSource(namespace: string, catalogSourceName: string, timeout = 60): Promise { - return new Promise(async (resolve, reject) => { - const watcher = new Watch(this.kubeConfig) - const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/catalogsources`, - { fieldSelector: `metadata.name=${catalogSourceName}` }, - (_phase: string, obj: any) => { - resolve(obj as CatalogSource) - }, - error => { - if (error) { - reject(error) - } - }) - - setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${catalogSourceName}" catalog source is created.`) - }, timeout * 1000) - }) - } - - async deleteCatalogSource(namespace: string, catalogSourceName: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', catalogSourceName) - } catch (e) { - if (e.response.statusCode === 404) { - return - } - throw this.wrapK8sClientError(e) - } - } - - async operatorGroupExists(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', name) - return this.compare(body, name) - } catch { - return false - } - } - - async createOperatorGroup(operatorGroupName: string, namespace: string) { - const operatorGroup: OperatorGroup = { - apiVersion: 'operators.coreos.com/v1', - kind: 'OperatorGroup', - metadata: { - name: operatorGroupName, - namespace, - }, - spec: { - targetNamespaces: [namespace], - }, - } - - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', operatorGroup) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteOperatorGroup(operatorGroupName: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', operatorGroupName) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async createOperatorSubscription(subscription: Subscription) { - const namespace: string = subscription.metadata.namespace! - - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', subscription) - return body - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getOperatorSubscription(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', name) - return body as Subscription - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async operatorSubscriptionExists(name: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', name) - return this.compare(body, name) - } catch { - return false - } - } - - async deleteOperatorSubscription(operatorSubscriptionName: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', operatorSubscriptionName) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async waitOperatorSubscriptionReadyForApproval(namespace: string, subscriptionName: string, timeout = AWAIT_TIMEOUT_S): Promise { - return new Promise(async (resolve, reject) => { - const watcher = new Watch(this.kubeConfig) - const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/subscriptions`, - { fieldSelector: `metadata.name=${subscriptionName}` }, - (_phase: string, obj: any) => { - const subscription = obj as Subscription - if (subscription.status && subscription.status.conditions) { - for (const condition of subscription.status.conditions) { - if (condition.type === 'InstallPlanPending' && condition.status === 'True') { - resolve(subscription.status.installplan) - } - } - } - }, - error => { - if (error) { - reject(error) - } - }) - - setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${subscriptionName}" subscription is ready.`) - }, timeout * 1000) - }) - } - - async approveOperatorInstallationPlan(name = '', namespace = '') { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const patch: InstallPlan = { - spec: { - approved: true, - }, - } - await customObjectsApi.patchNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'installplans', name, patch, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/merge-patch+json' } }) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async waitUntilOperatorIsInstalled(installPlanName: string, namespace: string, timeout = 240) { - return new Promise(async (resolve, reject) => { - const watcher = new Watch(this.kubeConfig) - const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/installplans`, - { fieldSelector: `metadata.name=${installPlanName}` }, - (_phase: string, obj: any) => { - const installPlan = obj as InstallPlan - if (installPlan.status && installPlan.status.phase === 'Failed') { - const errorMessage = [] - for (const condition of installPlan.status.conditions) { - if (!condition.reason) { - errorMessage.push(`Reason: ${condition.reason}`) - errorMessage.push(!condition.message ? `Message: ${condition.message}` : '') - } - } - reject(errorMessage.join(' ')) - } - if (installPlan.status && installPlan.status.conditions) { - for (const condition of installPlan.status.conditions) { - if (condition.type === 'Installed' && condition.status === 'True') { - resolve(installPlan) - } - } - } - }, - error => { - if (error) { - reject(error) - } - }) - - setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${installPlanName}" has go status 'Installed'.`) - }, timeout * 1000) - }) - } - - async getCSV(csvName: string, namespace: string): Promise { - const csvs = await this.getClusterServiceVersions(namespace) - return csvs.items.find(item => item.metadata.name === csvName) - } - - async getClusterServiceVersions(namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.listNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions') - return body as ClusterServiceVersionList - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async patchClusterServiceVersion(namespace: string, name: string, jsonPatch: any[]): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - const requestOptions = { - headers: { - 'content-type': 'application/json-patch+json', - }, - } - try { - const response = await customObjectsApi.patchNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions', name, jsonPatch, undefined, undefined, undefined, requestOptions) - return response.body as ClusterServiceVersion - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteClusterServiceVersion(namespace: string, csvName: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions', csvName) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async getPackageManifect(name: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - try { - const { body } = await customObjectsApi.getNamespacedCustomObject('packages.operators.coreos.com', 'v1', 'default', 'packagemanifests', name) - return body as PackageManifest - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async deleteNamespace(namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - await k8sCoreApi.deleteNamespace(namespace) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - /** - * Returns CRD version of Cert Manager - */ - async getCertManagerK8sApiVersion(): Promise { - return this.getCrdStorageVersion('certificates.cert-manager.io') - } - - async clusterIssuerExists(name: string, version: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - // If cluster issuers doesn't exist an exception will be thrown - await customObjectsApi.getClusterCustomObject('cert-manager.io', version, 'clusterissuers', name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async isNamespacedCertificateExists(name: string, version: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - // If cluster issuers doesn't exist an exception will be thrown - await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'certificates', name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async deleteNamespacedCertificate(name: string, version: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - // If cluster certificates doesn't exist an exception will be thrown - await customObjectsApi.deleteNamespacedCustomObject('cert-manager.io', version, namespace, 'certificates', name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async deleteNamespacedIssuer(name: string, version: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - await customObjectsApi.deleteNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', name) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async listClusterIssuers(version: string, labelSelector?: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - let res - try { - res = await customObjectsApi.listClusterCustomObject('cert-manager.io', version, 'clusterissuers', undefined, undefined, undefined, labelSelector) - } catch (e) { - throw this.wrapK8sClientError(e) - } - - if (!res || !res.body) { - throw new Error('Unable to get cluster issuers list') - } - const clusterIssuersList: { items?: any[] } = res.body - - return clusterIssuersList.items || [] - } - - async createCheClusterIssuer(cheClusterIssuerYamlPath: string, version: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - const cheClusterIssuer = this.safeLoadFromYamlFile(cheClusterIssuerYamlPath) - try { - await customObjectsApi.createClusterCustomObject('cert-manager.io', version, 'clusterissuers', cheClusterIssuer) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async createCertificateIssuer(cheClusterIssuerYamlPath: string, version: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - const certificateIssuer = this.safeLoadFromYamlFile(cheClusterIssuerYamlPath) - try { - await customObjectsApi.createNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', certificateIssuer) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async isCertificateIssuerExists(name: string, version: string, namespace: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - // If issuers doesn't exist an exception will be thrown - await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', name) - return true - } catch (e) { - if (e.response.statusCode === 404) { - return false - } - - throw this.wrapK8sClientError(e) - } - } - - async createCheClusterCertificate(certificate: V1Certificate, version: string): Promise { - const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) - - try { - await customObjectsApi.createNamespacedCustomObject('cert-manager.io', version, certificate.metadata.namespace, 'certificates', certificate) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async currentContext(): Promise { - return this.kubeConfig.getCurrentContext() - } - - getContext(name: string): Context | null { - return this.kubeConfig.getContextObject(name) - } - - /** - * Retrieve the default token from the default serviceAccount. - */ - async getDefaultServiceAccountToken(): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - const namespaceName = 'default' - const saName = 'default' - let res - // now get the matching secrets - try { - res = await k8sCoreApi.listNamespacedSecret(namespaceName) - } catch (e) { - throw this.wrapK8sClientError(e) - } - if (!res || !res.body) { - throw new Error('Unable to get default service account') - } - const v1SecretList = res.body - - if (!v1SecretList.items || v1SecretList.items.length === 0) { - throw new Error(`Unable to get default service account token since there is no secret in '${namespaceName}' namespace`) - } - - const v1DefaultSATokenSecret = v1SecretList.items.find(secret => secret.metadata!.annotations && - secret.metadata!.annotations['kubernetes.io/service-account.name'] === saName && - secret.type === 'kubernetes.io/service-account-token') - - if (!v1DefaultSATokenSecret) { - throw new Error(`Secret for '${saName}' service account is not found in namespace '${namespaceName}'`) - } - - return Buffer.from(v1DefaultSATokenSecret.data!.token, 'base64').toString() - } - - async checkKubeApi() { - const currentCluster = this.kubeConfig.getCurrentCluster() - if (!currentCluster) { - throw new Error(`The current context is unknown. It should be set using '${getClusterClientCommand()} config use-context ' or in another way.`) - } - - try { - await this.requestKubeHealthz(currentCluster) - } catch (error) { - if (error.message && (error.message as string).includes('E_K8S_API_UNAUTHORIZED')) { - const token = await this.getDefaultServiceAccountToken() - await this.requestKubeHealthz(currentCluster, token) - } else { - throw error - } - } - } - - async requestKubeHealthz(currentCluster: Cluster, token?: string) { - const endpoint = `${currentCluster.server}/healthz` - - try { - const config: AxiosRequestConfig = { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - requestCert: true, - }), - headers: token && { Authorization: 'bearer ' + token }, - } - - const response = await axios.get(`${endpoint}`, config) - if (!response || response.status !== 200 || response.data !== 'ok') { - throw new Error('E_BAD_RESP_K8S_API') - } - } catch (error) { - if (error.response && error.response.status === 403) { - throw new Error(`E_K8S_API_FORBIDDEN - Message: ${error.response.data.message}`) - } - if (error.response && error.response.status === 401) { - throw new Error(`E_K8S_API_UNAUTHORIZED - Message: ${error.response.data.message}`) - } - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - throw new Error(`E_K8S_API_UNKNOWN_ERROR - 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 - throw new Error(`E_K8S_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`) - } else { - // Something happened in setting up the request that triggered an Error - throw new Error(`E_CHECTL_UNKNOWN_ERROR - Message: ${error.message}`) - } - } - } - - async isOpenShift(): Promise { - return this.IsAPIGroupSupported('apps.openshift.io') - } - - async isOpenShift3(): Promise { - const isAppsAPISupported = await this.IsAPIGroupSupported('apps.openshift.io') - const isConfigAPISupported = await this.IsAPIGroupSupported('config.openshift.io') - return isAppsAPISupported && !isConfigAPISupported - } - - async isOpenShift4(): Promise { - const isRouteAPISupported = await this.IsAPIGroupSupported('route.openshift.io') - const isConfigAPISupported = await this.IsAPIGroupSupported('config.openshift.io') - return isRouteAPISupported && isConfigAPISupported - } - - async IsAPIExtensionSupported(version: string): Promise { - return this.IsAPIGroupSupported('apiextensions.k8s.io', version) - } - - async IsAPIGroupSupported(name: string, version?: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(ApisApi) - try { - const res = await k8sCoreApi.getAPIVersions() - if (!res || !res.body || !res.body.groups) { - return false - } - - const group = res.body.groups.find(g => g.name === name) - if (!group) { - return false - } - - if (version) { - return Boolean(group.versions.find(v => v.version === version)) - } - return Boolean(group) - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getIngressHost(name = '', namespace = ''): Promise { - const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) - try { - const res = await k8sExtensionsApi.readNamespacedIngress(name, namespace) - if (res && res.body && - res.body.spec && - res.body.spec.rules && - res.body.spec.rules.length > 0) { - return res.body.spec.rules[0].host || '' - } - throw new Error('ERR_INGRESS_NO_HOST') - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getIngressProtocol(name = '', namespace = ''): Promise { - const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) - try { - const res = await k8sExtensionsApi.readNamespacedIngress(name, namespace) - if (!res || !res.body || !res.body.spec) { - throw new Error('ERR_INGRESS_NO_HOST') - } - if (res.body.spec.tls && res.body.spec.tls.length > 0) { - return 'https' - } - return 'http' - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async getIngressesBySelector(labelSelector = '', namespace = ''): Promise { - const k8sV1Beta = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) - try { - const res = await k8sV1Beta.listNamespacedIngress(namespace, 'true', undefined, undefined, undefined, labelSelector) - if (res && res.body) { - return res.body - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - throw new Error('ERR_LIST_INGRESSES') - } - - async getSecret(name = '', namespace = 'default'): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - - // now get the matching secrets - try { - const res = await k8sCoreApi.readNamespacedSecret(name, namespace) - if (res && res.body && res.body) { - return res.body - } - } catch { - - } - } - - /** - * Creates a secret with given name and data. - * Data should not be base64 encoded. - */ - async createSecret(name: string, data: { [key: string]: string }, namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - - const secret = new V1Secret() - secret.metadata = new V1ObjectMeta() - secret.metadata.name = name - secret.metadata.namespace = namespace - secret.stringData = data - - try { - return (await k8sCoreApi.createNamespacedSecret(namespace, secret)).body - } catch { - - } - } - - /** - * Awaits secret to be present and contain non-empty data fields specified in dataKeys parameter. - */ - async waitSecret(secretName: string, namespace: string, dataKeys: string[] = [], timeout = AWAIT_TIMEOUT_S): Promise { - return new Promise(async (resolve, reject) => { - // Set up watcher - const watcher = new Watch(this.kubeConfig) - const request = await watcher - .watch(`/api/v1/namespaces/${namespace}/secrets/`, { fieldSelector: `metadata.name=${secretName}` }, - (_phase: string, obj: any) => { - const secret = obj as V1Secret - - // Check all required data fields to be present - if (dataKeys.length > 0 && secret.data) { - for (const key of dataKeys) { - if (!secret.data[key]) { - // Key is missing or empty - return - } - } - } - - // The secret with all specified fields is present, stop watching - if (request) { - request.abort() - } - // Release awaiter - resolve() - }, - error => { - if (error) { - reject(error) - } - }) - - // Automatically stop watching after timeout - const timeoutHandler = setTimeout(() => { - request.abort() - reject(`Timeout reached while waiting for "${secretName}" secret.`) - }, timeout * 1000) - - // Request secret, for case if it is already exist - const secret = await this.getSecret(secretName, namespace) - if (secret) { - // Stop watching - request.abort() - clearTimeout(timeoutHandler) - - // Relese awaiter - resolve() - } - }) - } - - async persistentVolumeClaimExist(name = '', namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const { body } = await k8sCoreApi.readNamespacedPersistentVolumeClaim(name, namespace) - return this.compare(body, name) - } catch { - return false - } - } - - async deletePersistentVolumeClaim(name: string, namespace: string): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - await k8sCoreApi.deleteNamespacedPersistentVolumeClaim(name, namespace) - } catch (e) { - if (e.response.statusCode !== 404) { - throw this.wrapK8sClientError(e) - } - } - } - - async getPersistentVolumeClaimsBySelector(labelSelector = '', namespace = ''): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sCoreApi.listNamespacedPersistentVolumeClaim(namespace, 'true', undefined, undefined, undefined, labelSelector) - if (res && res.body) { - return res.body - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - throw new Error('ERR_LIST_PVCS') - } - - async listNamespace(): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sApi.listNamespace() - if (res && res.body) { - return res.body - } - return { - items: [], - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - async listNamespacedPod(namespace: string, fieldSelector?: string, labelSelector?: string): Promise { - const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) - try { - const res = await k8sApi.listNamespacedPod(namespace, undefined, undefined, undefined, fieldSelector, labelSelector) - if (res && res.body) { - return res.body - } - return { - items: [], - } - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - /** - * Reads log by chunk and writes into a file. - */ - async readNamespacedPodLog(pod: string, namespace: string, container: string, filename: string, follow: boolean): Promise { - return new Promise(async (resolve, reject) => { - const logHelper = new Log(this.kubeConfig) - const stream = new Writable() - stream._write = function (chunk, encoding, done) { - fs.appendFileSync(filename, chunk, { encoding }) - done() - } - - await logHelper.log(namespace, pod, container, stream, error => { - stream.end() - if (error) { - reject(error) - } else { - resolve() - } - }, { follow }) - }) - } - - /** - * Forwards port, based on the example - * https://github.com/kubernetes-client/javascript/blob/master/examples/typescript/port-forward/port-forward.ts - */ - async portForward(podName: string, namespace: string, port: number): Promise { - const portForwardHelper = new PortForward(this.kubeConfig, true) - try { - const server = net.createServer(async socket => { - await portForwardHelper.portForward(namespace, podName, [port], socket, null, socket) - }) - server.listen(port, 'localhost') - return - } catch (e) { - throw this.wrapK8sClientError(e) - } - } - - /** - * Checks if message is present and returns error with it - * or returns error with the specified error if message is not found. - * - * @param e k8s error to wrap - */ - private wrapK8sClientError(e: any): Error { - if (e.response && e.response.body) { - return newError(e.response.body, e) - } - return e - } - - public safeLoadFromYamlFile(filePath: string): any { - return safeLoadFromYamlFile(filePath) - } + public readonly kubeConfig + + readonly API_EXTENSIONS_V1BETA1 = 'apiextensions.k8s.io/v1beta1' + + podWaitTimeout: number + + podDownloadImageTimeout: number + + podReadyTimeout: number + + podErrorRecheckTimeout: number + + constructor(flags?: any) { + this.podWaitTimeout = (flags && flags.k8spodwaittimeout) ? parseInt(flags.k8spodwaittimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT + this.podReadyTimeout = (flags && flags.k8spodreadytimeout) ? parseInt(flags.k8spodreadytimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT + this.podDownloadImageTimeout = (flags && flags.k8spoddownloadimagetimeout) ? parseInt(flags.k8spoddownloadimagetimeout, 10) : DEFAULT_K8S_POD_WAIT_TIMEOUT + this.podErrorRecheckTimeout = (flags && flags.spoderrorrechecktimeout) ? parseInt(flags.spoderrorrechecktimeout, 10) : DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT + this.kubeConfig = new KubeConfig() + this.kubeConfig.loadFromDefault() + } + + async createNamespace(namespaceName: string, labels: any): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + const namespaceObject = { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { + labels, + name: namespaceName, + }, + } + + try { + await k8sCoreApi.createNamespace(namespaceObject) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteAllServices(namespace: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const res = await k8sApi.listNamespacedService(namespace) + if (res && res.response && res.response.statusCode === 200) { + const serviceList = res.body + await serviceList.items.forEach(async service => { + try { + await k8sApi.deleteNamespacedService(service.metadata!.name!, namespace) + } catch (error) { + if (error.response.statusCode !== 404) { + throw error + } + } + }) + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async applyResource(yamlPath: string, opts = ''): Promise { + const command = `kubectl apply -f ${yamlPath} ${opts}` + await execa(command, { timeout: 30000, shell: true }) + } + + async getServicesBySelector(labelSelector = '', namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const res = await k8sCoreApi.listNamespacedService(namespace, 'true', undefined, undefined, undefined, labelSelector) + if (res && res.body) { + return res.body + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + throw new Error('ERR_LIST_SERVICES') + } + + async waitForService(selector: string, namespace = '', intervalMs = 500, timeoutMs = 30000) { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + const currentServices = await this.getServicesBySelector(selector, namespace) + if (currentServices && currentServices.items.length > 0) { + return + } + await cli.wait(intervalMs) + } + throw new Error(`ERR_TIMEOUT: Timeout set to waiting for service ${timeoutMs}`) + } + + async serviceAccountExist(name = '', namespace = ''): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body } = await k8sApi.readNamespacedServiceAccount(name, namespace) + return this.compare(body, name) + } catch { + return false + } + } + + async createServiceAccount(name = '', namespace = '') { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + const sa = new V1ServiceAccount() + sa.metadata = new V1ObjectMeta() + sa.metadata.name = name + sa.metadata.namespace = namespace + try { + return await k8sCoreApi.createNamespacedServiceAccount(namespace, sa) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async waitServiceAccount(name: string, namespace: string, timeout = AWAIT_TIMEOUT_S): Promise { + return new Promise(async (resolve, reject) => { + // Set up watcher + const watcher = new Watch(this.kubeConfig) + const request = await watcher + .watch(`/api/v1/namespaces/${namespace}/serviceaccounts`, {}, + (_phase: string, obj: any) => { + const serviceAccount = obj as V1ServiceAccount + + // Filter other service accounts in the given namespace + if (serviceAccount && serviceAccount.metadata && serviceAccount.metadata.name === name) { + // The service account is present, stop watching + if (request) { + request.abort() + } + // Release awaiter + resolve() + } + }, + error => { + if (error) { + reject(error) + } + }) + + // Automatically stop watching after timeout + const timeoutHandler = setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${name}" service account.`) + }, timeout * 1000) + + // Request service account, for case if it is already exist + const serviceAccount = await this.getSecret(name, namespace) + if (serviceAccount) { + // Stop watching + request.abort() + clearTimeout(timeoutHandler) + + // Relese awaiter + resolve() + } + }) + } + + async deleteServiceAccount(name: string, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sCoreApi.deleteNamespacedServiceAccount(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async createServiceAccountFromFile(filePath: string, namespace = '') { + const yamlServiceAccount = this.safeLoadFromYamlFile(filePath) as V1ServiceAccount + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + return await k8sCoreApi.createNamespacedServiceAccount(namespace, yamlServiceAccount) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceServiceAccountFromFile(filePath: string, namespace = '') { + const yamlServiceAccount = this.safeLoadFromYamlFile(filePath) as V1ServiceAccount + if (!yamlServiceAccount || !yamlServiceAccount.metadata || !yamlServiceAccount.metadata.name) { + throw new Error(`Service account read from ${filePath} must have name specified.`) + } + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + return await k8sCoreApi.replaceNamespacedServiceAccount(yamlServiceAccount.metadata.name, namespace, yamlServiceAccount) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async roleExist(name = '', namespace = ''): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const { body } = await k8sRbacAuthApi.readNamespacedRole(name, namespace) + return this.compare(body, name) + } catch { + return false + } + } + + async clusterRoleExist(name = ''): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const { body } = await k8sRbacAuthApi.readClusterRole(name) + return this.compare(body, name) + } catch { + return false + } + } + + async getClusterRole(name: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const { body } = await k8sRbacAuthApi.readClusterRole(name) + return body + } catch { + return + } + } + + async getRole(name: string, namespace: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.readNamespacedRole(name, namespace) + return res.body + } catch (e) { + if (e.statusCode === 404) { + return + } + throw this.wrapK8sClientError(e) + } + } + + async listRoles(namespace: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.listNamespacedRole(namespace) + return res.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createRoleFrom(yamlRole: V1Role, namespace: string) { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.createNamespacedRole(namespace, yamlRole) + return res.response.statusCode + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createRoleFromFile(filePath: string, namespace: string) { + const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role + return this.createRoleFrom(yamlRole, namespace) + } + + async replaceRoleFrom(yamlRole: V1Role, namespace: string) { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + + if (!yamlRole.metadata || !yamlRole.metadata.name) { + throw new Error('Role object requires name') + } + try { + const res = await k8sRbacAuthApi.replaceNamespacedRole(yamlRole.metadata.name, namespace, yamlRole) + return res.response.statusCode + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceRoleFromFile(filePath: string, namespace: string) { + const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role + return this.replaceRoleFrom(yamlRole, namespace) + } + + async listClusterRoles(): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.listClusterRole() + return res.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + if (!yamlClusterRole.metadata) { + yamlClusterRole.metadata = {} + } + + if (clusterRoleName) { + yamlClusterRole.metadata.name = clusterRoleName + } else if (!yamlClusterRole.metadata.name) { + throw new Error('Role name is not specified') + } + try { + const res = await k8sRbacAuthApi.createClusterRole(yamlClusterRole) + return res.response.statusCode + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createClusterRoleFromFile(filePath: string, clusterRoleName?: string) { + const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole + return this.createClusterRoleFrom(yamlClusterRole, clusterRoleName) + } + + async replaceClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + if (!yamlClusterRole.metadata) { + yamlClusterRole.metadata = {} + } + + if (clusterRoleName) { + yamlClusterRole.metadata.name = clusterRoleName + } else if (!yamlClusterRole.metadata.name) { + throw new Error('Role name is not specified') + } + try { + const res = await k8sRbacAuthApi.replaceClusterRole(yamlClusterRole.metadata.name, yamlClusterRole) + return res.response.statusCode + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceClusterRoleFromFile(filePath: string, clusterRoleName?: string) { + const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole + return this.replaceClusterRoleFrom(yamlClusterRole, clusterRoleName) + } + + async addClusterRoleRule(name: string, apiGroups: string[], resources: string[], verbs: string[]): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + const clusterRole = await this.getClusterRole(name) + if (clusterRole) { + // Clean up metadata, otherwise replace role call will fail + clusterRole.metadata = {} + clusterRole.metadata.name = name + + // Add new policy + const additionaRule = new V1PolicyRule() + additionaRule.apiGroups = apiGroups + additionaRule.resources = resources + additionaRule.verbs = verbs + if (clusterRole.rules) { + clusterRole.rules.push(additionaRule) + } + + try { + const { body } = await k8sRbacAuthApi.replaceClusterRole(name, clusterRole) + return body + } catch { + return + } + } + } + + async deleteRole(name: string, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + await k8sCoreApi.deleteNamespacedRole(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async getPodListByLabel(namespace: string, labelSelector: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body: podList } = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, labelSelector) + + return podList.items + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteClusterRole(name: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + await k8sCoreApi.deleteClusterRole(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async listRoleBindings(namespace: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.listNamespacedRoleBinding(namespace) + return res.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async roleBindingExist(name = '', namespace = ''): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + await k8sRbacAuthApi.readNamespacedRoleBinding(name, namespace) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async isMutatingWebhookConfigurationExists(name: string): Promise { + const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) + try { + await k8sAdmissionApi.readMutatingWebhookConfiguration(name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async getMutatingWebhookConfiguration(name: string): Promise { + const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) + try { + const res = await k8sAdmissionApi.readMutatingWebhookConfiguration(name) + return res.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async isValidatingWebhookConfigurationExists(name: string): Promise { + const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) + try { + await k8sAdmissionApi.readValidatingWebhookConfiguration(name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async deleteValidatingWebhookConfiguration(name: string): Promise { + const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) + try { + await k8sAdmissionApi.deleteValidatingWebhookConfiguration(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async deleteMutatingWebhookConfiguration(name: string): Promise { + const k8sAdmissionApi = this.kubeConfig.makeApiClient(AdmissionregistrationV1Api) + try { + await k8sAdmissionApi.deleteMutatingWebhookConfiguration(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async listClusterRoleBindings(labelSelector?: string, fieldSelector?: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const res = await k8sRbacAuthApi.listClusterRoleBinding(undefined, undefined, undefined, fieldSelector, labelSelector) + return res.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async clusterRoleBindingExist(name: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const { body } = await k8sRbacAuthApi.readClusterRoleBinding(name) + return this.compare(body, name) + } catch { + return false + } + } + + async createAdminRoleBinding(name = '', serviceAccount = '', namespace = '') { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + const rb = new V1RoleBinding() + rb.metadata = new V1ObjectMeta() + rb.metadata.name = name + rb.metadata.namespace = namespace + rb.roleRef = new V1RoleRef() + rb.roleRef.kind = 'ClusterRole' + rb.roleRef.name = 'admin' + const subject = new V1Subject() + subject.kind = 'ServiceAccount' + subject.name = serviceAccount + subject.namespace = namespace + rb.subjects = [subject] + try { + return await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, rb) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const response = await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, yamlRoleBinding) + return response.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createRoleBindingFromFile(filePath: string, namespace: string): Promise { + const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding + return this.createRoleBindingFrom(yamlRoleBinding, namespace) + } + + async replaceRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { + if (!yamlRoleBinding.metadata || !yamlRoleBinding.metadata.name) { + throw new Error('RoleBinding object requires name') + } + + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + const response = await k8sRbacAuthApi.replaceNamespacedRoleBinding(yamlRoleBinding.metadata.name, namespace, yamlRoleBinding) + return response.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceRoleBindingFromFile(filePath: string, namespace: string): Promise { + const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding + return this.replaceRoleBindingFrom(yamlRoleBinding, namespace) + } + + async createClusterRoleBindingFrom(yamlClusterRoleBinding: V1ClusterRoleBinding) { + if (!yamlClusterRoleBinding.metadata || !yamlClusterRoleBinding.metadata.name) { + throw new Error('ClusterRoleBinding object requires name') + } + + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + return await k8sRbacAuthApi.createClusterRoleBinding(yamlClusterRoleBinding) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createClusterRoleBinding(name: string, saName: string, saNamespace = '', roleName = '') { + const clusterRoleBinding = { + apiVersion: 'rbac.authorization.k8s.io/v1', + metadata: { + name: `${name}`, + }, + subjects: [ + { + kind: 'ServiceAccount', + name: `${saName}`, + namespace: `${saNamespace}`, + }, + ], + roleRef: { + kind: 'ClusterRole', + name: `${roleName}`, + apiGroup: 'rbac.authorization.k8s.io', + }, + } as V1ClusterRoleBinding + return this.createClusterRoleBindingFrom(clusterRoleBinding) + } + + async replaceClusterRoleBindingFrom(clusterRoleBinding: V1ClusterRoleBinding) { + if (!clusterRoleBinding.metadata || !clusterRoleBinding.metadata.name) { + throw new Error('Cluster Role Binding must have name specified') + } + + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + return await k8sRbacAuthApi.replaceClusterRoleBinding(clusterRoleBinding.metadata.name, clusterRoleBinding) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceClusterRoleBinding(name: string, saName: string, saNamespace = '', roleName = '') { + const clusterRoleBinding = { + apiVersion: 'rbac.authorization.k8s.io/v1', + metadata: { + name: `${name}`, + }, + subjects: [ + { + kind: 'ServiceAccount', + name: `${saName}`, + namespace: `${saNamespace}`, + }, + ], + roleRef: { + kind: 'ClusterRole', + name: `${roleName}`, + apiGroup: 'rbac.authorization.k8s.io', + }, + } as V1ClusterRoleBinding + return this.replaceClusterRoleBindingFrom(clusterRoleBinding) + } + + async deleteRoleBinding(name: string, namespace: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + await k8sRbacAuthApi.deleteNamespacedRoleBinding(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async deleteClusterRoleBinding(name: string): Promise { + const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) + try { + await k8sRbacAuthApi.deleteClusterRoleBinding(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async getConfigMap(name = '', namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body } = await k8sCoreApi.readNamespacedConfigMap(name, namespace) + return this.compare(body, name) && body + } catch { + return + } + } + + async getConfigMapValue(name: string, namespace: string, key: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body } = await k8sCoreApi.readNamespacedConfigMap(name, namespace) + if (body.data) { + return body.data[key] + } + } catch { + return + } + } + + async createConfigMapFromFile(filePath: string, namespace = '') { + const yamlConfigMap = this.safeLoadFromYamlFile(filePath) as V1ConfigMap + return this.createNamespacedConfigMap(namespace, yamlConfigMap) + } + + public async createNamespacedConfigMap(namespace: string, configMap: V1ConfigMap) { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + + try { + const { body } = await k8sCoreApi.createNamespacedConfigMap(namespace, configMap) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async patchConfigMap(name: string, patch: any, namespace = '') { + const k8sCoreApi = this.kubeConfig.makeApiClient(PatchedK8sApi) + try { + return await k8sCoreApi.patchNamespacedConfigMap(name, namespace, patch) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteConfigMap(name: string, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sCoreApi.deleteNamespacedConfigMap(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + public async replaceNamespacedConfigMap(name: string, namespace: string, configMap: V1ConfigMap) { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + + try { + const { body } = await k8sCoreApi.replaceNamespacedConfigMap(name, namespace, configMap) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getNamespace(namespace: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body } = await k8sApi.readNamespace(namespace) + return body + } catch { + } + } + + async hasReadPermissionsForNamespace(namespace: string): Promise { + const k8sApi = this.kubeConfig.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 = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const res = await k8sCoreApi.readNamespacedPod(podName, namespace) + if (res && res.body) { + return res.body + } + } catch { + return + } + } + + async patchCustomResource(name: string, namespace: string, patch: any, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + // It is required to patch content-type, otherwise request will be rejected with 415 (Unsupported media type) error. + const requestOptions = { + headers: { + 'content-type': 'application/merge-patch+json', + }, + } + + try { + const res = await k8sCoreApi.patchNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural, name, patch, undefined, undefined, undefined, requestOptions) + if (res && res.body) { + return res.body + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async patchNamespacedPod(name: string, namespace: string, patch: any): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + + // It is required to patch content-type, otherwise request will be rejected with 415 (Unsupported media type) error. + const requestOptions = { + headers: { + 'content-type': 'application/strategic-merge-patch+json', + }, + } + + try { + const res = await k8sCoreApi.patchNamespacedPod(name, namespace, patch, undefined, undefined, undefined, undefined, requestOptions) + if (res && res.body) { + return res.body + } + } catch { + return + } + } + + async podsExistBySelector(selector: string, namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + let res + try { + res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) + } catch (e) { + throw this.wrapK8sClientError(e) + } + + if (!res || !res.body || !res.body.items) { + throw new Error(`Get pods by selector "${selector}" returned an invalid response`) + } + + return (res.body.items.length > 0) + } + + /** + * Returns pod waiting state. + */ + async getPodWaitingState(namespace: string, selector: string, desiredPhase: string): Promise { + const pods = await this.getPodListByLabel(namespace, selector) + if (!pods.length) { + return + } + + for (const pod of pods) { + if (pod.status && pod.status.phase === desiredPhase && pod.status.containerStatuses) { + for (const status of pod.status.containerStatuses) { + if (status.state && status.state.waiting && status.state.waiting.message && status.state.waiting.reason) { + return status.state.waiting + } + } + } + } + } + + /** + * Returns pod last terminated state. + */ + async getPodLastTerminatedState(namespace: string, selector: string): Promise { + const pods = await this.getPodListByLabel(namespace, selector) + if (!pods.length) { + return + } + + for (const pod of pods) { + if (pod.status && pod.status.containerStatuses) { + for (const status of pod.status.containerStatuses) { + if (status.lastState) { + return status.lastState.terminated + } + } + } + } + } + + async getPodCondition(namespace: string, selector: string, conditionType: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + let res + try { + res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) + } catch (e) { + throw this.wrapK8sClientError(e) + } + + if (!res || !res.body || !res.body.items) { + return [] + } + + const conditions: V1PodCondition[] = [] + for (const pod of res.body.items) { + if (pod.status && pod.status.conditions) { + for (const condition of pod.status.conditions) { + if (condition.type === conditionType) { + conditions.push(condition) + } + } + } + } + + return conditions + } + + async getPodReadyConditionStatus(selector: string, namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + let res + try { + res = await k8sCoreApi.listNamespacedPod(namespace, undefined, undefined, undefined, undefined, selector) + } catch (e) { + throw this.wrapK8sClientError(e) + } + + if (!res || !res.body || !res.body.items) { + throw new Error(`Get pods by selector "${selector}" returned an invalid response.`) + } + + if (res.body.items.length < 1) { + // No pods found by the specified selector. So, it's not ready. + return 'False' + } + + if (res.body.items.length > 1) { + // Several pods found, rolling update? + return + } + + if (!res.body.items[0].status || !res.body.items[0].status.conditions || !(res.body.items[0].status.conditions.length > 0)) { + return + } + + const conditions = res.body.items[0].status.conditions + for (const condition of conditions) { + if (condition.type === 'Ready') { + return condition.status + } + } + } + + async waitForPodReady(selector: string, namespace = '', intervalMs = 500, timeoutMs = this.podReadyTimeout) { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + const readyStatus = await this.getPodReadyConditionStatus(selector, namespace) + if (readyStatus === 'True') { + return + } + await cli.wait(intervalMs) + } + throw new Error(`ERR_TIMEOUT: Timeout set to pod ready timeout ${this.podReadyTimeout}`) + } + + async waitUntilPodIsDeleted(selector: string, namespace = '', intervalMs = 500, timeoutMs = this.podReadyTimeout) { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + const pods = await this.listNamespacedPod(namespace, undefined, selector) + if (!pods.items.length) { + return + } + await cli.wait(intervalMs) + } + throw new Error('ERR_TIMEOUT: Waiting until pod is deleted took too long.') + } + + async deletePod(name: string, namespace = '') { + this.kubeConfig.loadFromDefault() + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + return await k8sCoreApi.deleteNamespacedPod(name, namespace) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + // make sure that flag is specified for command that it's invoked + async waitLatestReplica(deploymentName: string, namespace = '', intervalMs = 500, timeoutMs = this.podWaitTimeout) { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + const deployment = await this.getDeployment(deploymentName, namespace) + if (!deployment) { + throw new Error(`Deployment ${namespace}/${deploymentName} is not found.`) + } + + const deploymentStatus = deployment.status + if (!deploymentStatus) { + throw new Error(`Deployment ${namespace}/${deploymentName} does not have any status`) + } + + if (deploymentStatus.unavailableReplicas && deploymentStatus.unavailableReplicas > 0) { + await cli.wait(intervalMs) + } else { + return + } + } + + throw new Error(`ERR_TIMEOUT: Timeout set to pod wait timeout ${this.podWaitTimeout}`) + } + + async deploymentExist(name = '', namespace = ''): Promise { + const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const { body } = await k8sApi.readNamespacedDeployment(name, namespace) + return this.compare(body, name) + } catch { + return false + } + } + + async isConfigMapExists(name: string, namespace: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sApi.readNamespacedConfigMap(name, namespace) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async deploymentReady(name = '', namespace = ''): Promise { + const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const res = await k8sApi.readNamespacedDeployment(name, namespace) + return ((res && res.body && + res.body.status && res.body.status.readyReplicas && + res.body.status.readyReplicas > 0) as boolean) + } catch { + return false + } + } + + async deploymentStopped(name = '', namespace = ''): Promise { + const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const res = await k8sApi.readNamespacedDeployment(name, namespace) + if (res && res.body && res.body.spec && res.body.spec.replicas) { + throw new Error(`Deployment '${name}' without replicas in spec is fetched`) + } + return res.body!.spec!.replicas === 0 + } catch { + return false + } + } + + async isDeploymentPaused(name = '', namespace = ''): Promise { + const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const res = await k8sApi.readNamespacedDeployment(name, namespace) + if (!res || !res.body || !res.body.spec) { + throw new Error('E_BAD_DEPLOY_RESPONSE') + } + return res.body.spec.paused || false + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async pauseDeployment(name = '', namespace = '') { + const k8sApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) + try { + const patch = { + spec: { + paused: true, + }, + } + await k8sApi.patchNamespacedDeployment(name, namespace, patch) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async resumeDeployment(name = '', namespace = '') { + const k8sApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) + try { + const patch = { + spec: { + paused: false, + }, + } + await k8sApi.patchNamespacedDeployment(name, namespace, patch) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async scaleDeployment(name = '', namespace = '', replicas: number) { + const k8sAppsApi = this.kubeConfig.makeApiClient(PatchedK8sAppsApi) + const patch = { + spec: { + replicas, + }, + } + let res + try { + res = await k8sAppsApi.patchNamespacedDeploymentScale(name, namespace, patch) + } catch (e) { + throw this.wrapK8sClientError(e) + } + + if (!res || !res.body) { + throw new Error('Patch deployment scale returned an invalid response') + } + } + + async createDeployment(name: string, + image: string, + serviceAccount: string, + pullPolicy: string, + configMapEnvSource: string, + namespace: string) { + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + const deployment = new V1Deployment() + deployment.metadata = new V1ObjectMeta() + deployment.metadata.name = name + deployment.metadata.namespace = namespace + deployment.spec = new V1DeploymentSpec() + deployment.spec.selector = new V1LabelSelector() + deployment.spec.selector.matchLabels = { app: name } + deployment.spec.template = new V1PodTemplateSpec() + deployment.spec.template.metadata = new V1ObjectMeta() + deployment.spec.template.metadata.name = name + deployment.spec.template.metadata.labels = { app: name } + deployment.spec.template.spec = new V1PodSpec() + deployment.spec.template.spec.serviceAccountName = serviceAccount + const opContainer = new V1Container() + opContainer.name = name + opContainer.image = image + opContainer.imagePullPolicy = pullPolicy + const envFromSource = new V1EnvFromSource() + envFromSource.configMapRef = new V1ConfigMapEnvSource() + envFromSource.configMapRef.name = configMapEnvSource + opContainer.envFrom = [envFromSource] + deployment.spec.template.spec.containers = [opContainer] + + try { + return await k8sAppsApi.createNamespacedDeployment(namespace, deployment) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createDeploymentFrom(yamlDeployment: V1Deployment): Promise { + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + await k8sAppsApi.createNamespacedDeployment(yamlDeployment.metadata!.namespace!, yamlDeployment) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createServiceFrom(yamlService: V1Service, namespace = '') { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + return await k8sApi.createNamespacedService(namespace, yamlService) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceDeploymentFrom(yamlDeployment: V1Deployment): Promise { + // updating restartedAt to make sure that rollout will be restarted + let annotations = yamlDeployment.spec!.template!.metadata!.annotations + if (!annotations) { + annotations = {} + yamlDeployment.spec!.template!.metadata!.annotations = annotations + } + annotations['kubectl.kubernetes.io/restartedAt'] = new Date().toISOString() + + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + await k8sAppsApi.replaceNamespacedDeployment(yamlDeployment.metadata!.name!, yamlDeployment.metadata!.namespace!, yamlDeployment) + } catch (e) { + if (e.response && e.response.body && e.response.body.message && e.response.body.message.toString().endsWith('field is immutable')) { + try { + await k8sAppsApi.deleteNamespacedDeployment(yamlDeployment.metadata!.name!, yamlDeployment.metadata!.namespace!) + await k8sAppsApi.createNamespacedDeployment(yamlDeployment.metadata!.namespace!, yamlDeployment) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + throw this.wrapK8sClientError(e) + } + } + + async deleteAllDeployments(namespace: string): Promise { + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + await k8sAppsApi.deleteCollectionNamespacedDeployment(namespace) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getDeploymentsBySelector(labelSelector = '', namespace = ''): Promise { + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const res = await k8sAppsApi.listNamespacedDeployment(namespace, 'true', undefined, undefined, undefined, labelSelector) + if (res && res.body) { + return res.body + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + throw new Error('ERR_LIST_NAMESPACES') + } + + async getDeployment(name: string, namespace: string): Promise { + const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) + try { + const res = await k8sAppsApi.readNamespacedDeployment(name, namespace) + if (res && res.body) { + return res.body! + } + } catch (error) { + if (error.response && error.response.statusCode === 404) { + return + } + throw this.wrapK8sClientError(error) + } + throw new Error('ERR_GET_DEPLOYMENT') + } + + async createPod(name: string, + image: string, + serviceAccount: string, + restartPolicy: string, + pullPolicy: string, + configMapEnvSource: string, + namespace: string) { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + const pod = new V1Pod() + pod.metadata = new V1ObjectMeta() + pod.metadata.name = name + pod.metadata.labels = { app: name } + pod.metadata.namespace = namespace + pod.spec = new V1PodSpec() + pod.spec.restartPolicy = restartPolicy + pod.spec.serviceAccountName = serviceAccount + const opContainer = new V1Container() + opContainer.name = name + opContainer.image = image + opContainer.imagePullPolicy = pullPolicy + const envFromSource = new V1EnvFromSource() + envFromSource.configMapRef = new V1ConfigMapEnvSource() + envFromSource.configMapRef.name = configMapEnvSource + opContainer.envFrom = [envFromSource] + pod.spec.containers = [opContainer] + + try { + return await k8sCoreApi.createNamespacedPod(namespace, pod) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createJob(name: string, + image: string, + serviceAccount: string, + namespace: string, + backoffLimit = 0, + restartPolicy = 'Never') { + const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) + + const job = new V1Job() + job.metadata = new V1ObjectMeta() + job.metadata.name = name + job.metadata.labels = { app: name } + job.metadata.namespace = namespace + job.spec = new V1JobSpec() + job.spec.ttlSecondsAfterFinished = 10 + job.spec.backoffLimit = backoffLimit + job.spec.template = new V1PodTemplateSpec() + job.spec.template.spec = new V1PodSpec() + job.spec.template.spec.serviceAccountName = serviceAccount + const jobContainer = new V1Container() + jobContainer.name = name + jobContainer.image = image + job.spec.template.spec.restartPolicy = restartPolicy + job.spec.template.spec.containers = [jobContainer] + + try { + return await k8sBatchApi.createNamespacedJob(namespace, job) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getJob(jobName: string, namespace: string): Promise { + const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) + + try { + const result = await k8sBatchApi.readNamespacedJob(jobName, namespace) + return result.body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async waitJob(jobName: string, namespace: string, timeout = AWAIT_TIMEOUT_S): Promise { + return new Promise(async (resolve, reject) => { + // Set up watcher + const watcher = new Watch(this.kubeConfig) + const request = await watcher + .watch(`/apis/batch/v1/namespaces/${namespace}/jobs/`, {}, + (_phase: string, obj: any) => { + const job = obj as V1Job + + // Filter other jobs in the given namespace + if (job && job.metadata && job.metadata.name === jobName) { + // Check job status + if (job.status && job.status.succeeded && job.status.succeeded >= 1) { + // Job is finished, stop watching + if (request) { + request.abort() + } + // Release awaiter + resolve() + } + } + }, + error => { + if (error) { + reject(error) + } + }) + + // Automatically stop watching after timeout + const timeoutHandler = setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${jobName}" job.`) + }, timeout * 1000) + + // Request job, for case if it is already ready + const job = await this.getJob(jobName, namespace) + if (job.status && job.status.succeeded && job.status.succeeded >= 1) { + // Stop watching + request.abort() + clearTimeout(timeoutHandler) + + // Relese awaiter + resolve() + } + }) + } + + async deleteJob(jobName: string, namespace: string): Promise { + const k8sBatchApi = this.kubeConfig.makeApiClient(BatchV1Api) + + try { + const result = await k8sBatchApi.deleteNamespacedJob(jobName, namespace) + return result.body.status === 'Success' + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async compare(body: any, name: string): Promise { + if (body && body.metadata && body.metadata.name && body.metadata.name === name) { + return true + } else { + return false + } + } + + async ingressExist(name = '', namespace = ''): Promise { + const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) + try { + const { body } = await k8sExtensionsApi.readNamespacedIngress(name, namespace) + return this.compare(body, name) + } catch { + return false + } + } + + async deleteAllIngresses(namespace: string): Promise { + const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) + try { + await k8sExtensionsApi.deleteCollectionNamespacedIngress(namespace) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createCrdFromFile(filePath: string): Promise { + const yaml = this.safeLoadFromYamlFile(filePath) + if (yaml.apiVersion === this.API_EXTENSIONS_V1BETA1) { + return this.createCrdV1Beta1(yaml) + } + return this.createCrdV1(yaml) + } + + private async createCrdV1Beta1(yaml: any): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) + try { + await k8sApi.createCustomResourceDefinition(yaml) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + private async createCrdV1(yaml: any): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) + try { + await k8sApi.createCustomResourceDefinition(yaml) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async replaceCrdFromFile(filePath: string, resourceVersion: string): Promise { + const yaml = this.safeLoadFromYamlFile(filePath) + if (!yaml.metadata || !yaml.metadata.name) { + throw new Error(`Name is not defined in: ${filePath}`) + } + + yaml.metadata.resourceVersion = resourceVersion + if (yaml.apiVersion === this.API_EXTENSIONS_V1BETA1) { + return this.replaceCrdV1Beta1(yaml) + } + return this.replaceCrdV1(yaml) + } + + private async replaceCrdV1Beta1(yaml: any): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) + try { + await k8sApi.replaceCustomResourceDefinition(yaml.metadata.name, yaml) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + private async replaceCrdV1(yaml: any): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) + try { + await k8sApi.replaceCustomResourceDefinition(yaml.metadata.name, yaml) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getCrd(name: string): Promise { + if (await this.IsAPIExtensionSupported('v1')) { + return this.getCrdV1(name) + } + return this.getCrdV1beta1(name) + } + + private async getCrdV1(name: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) + try { + const { body } = await k8sApi.readCustomResourceDefinition(name) + return body + } catch (e) { + if (e.response.statusCode === 404) { + return + } + + throw this.wrapK8sClientError(e) + } + } + + private async getCrdV1beta1(name: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) + try { + const { body } = await k8sApi.readCustomResourceDefinition(name) + return body + } catch (e) { + if (e.response.statusCode === 404) { + return + } + + throw this.wrapK8sClientError(e) + } + } + + async getCrdStorageVersion(name: string): Promise { + const crd = await this.getCrd(name) + if (!crd.spec.versions) { + // Should never happen + return 'v1' + } + + const version = crd.spec.versions.find((v: any) => v.storage) + return version ? version.name : 'v1' + } + + async deleteCrd(name: string): Promise { + if (await this.IsAPIExtensionSupported('v1')) { + return this.deleteCrdV1(name) + } + return this.deleteCrdV1Beta1(name) + } + + private async deleteCrdV1Beta1(name: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1beta1Api) + try { + await k8sApi.deleteCustomResourceDefinition(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + private async deleteCrdV1(name: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(ApiextensionsV1Api) + try { + await k8sApi.deleteCustomResourceDefinition(name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async createCheCluster(cheClusterCR: any, flags: any, ctx: any, useDefaultCR: boolean): Promise { + const cheNamespace = flags.chenamespace + if (useDefaultCR) { + // If CheCluster CR is not explicitly provided, then modify the default example CR + // with values derived from the other parameters + + if (VersionHelper.isDeployingStableVersion(flags)) { + // Use images from operator defaults in case of a stable version + cheClusterCR.spec.server.cheImage = '' + cheClusterCR.spec.server.cheImageTag = '' + cheClusterCR.spec.server.pluginRegistryImage = '' + cheClusterCR.spec.server.devfileRegistryImage = '' + cheClusterCR.spec.auth.identityProviderImage = '' + } + const cheImage = flags.cheimage + if (cheImage) { + const imageAndTag = cheImage.split(':', 2) + cheClusterCR.spec.server.cheImage = imageAndTag[0] + cheClusterCR.spec.server.cheImageTag = imageAndTag.length === 2 ? imageAndTag[1] : 'latest' + } + + if ((flags.installer === 'olm' && !flags['catalog-source-yaml']) || (flags['catalog-source-yaml'] && flags['olm-channel'] === OLM_STABLE_CHANNEL_NAME)) { + // use default image tag for `olm` to install stable Che, because we don't have nightly channel for OLM catalog. + cheClusterCR.spec.server.cheImageTag = '' + } + cheClusterCR.spec.server.cheDebug = flags.debug ? flags.debug.toString() : 'false' + + if (isKubernetesPlatformFamily(flags.platform) || !cheClusterCR.spec.auth.openShiftoAuth) { + cheClusterCR.spec.auth.updateAdminPassword = true + } + + if (!cheClusterCR.spec.k8s) { + cheClusterCR.spec.k8s = {} + } + if (flags.tls) { + cheClusterCR.spec.server.tlsSupport = flags.tls + if (!cheClusterCR.spec.k8s.tlsSecretName) { + cheClusterCR.spec.k8s.tlsSecretName = 'che-tls' + } + } + if (flags.domain) { + cheClusterCR.spec.k8s.ingressDomain = flags.domain + } + const pluginRegistryUrl = flags['plugin-registry-url'] + if (pluginRegistryUrl) { + cheClusterCR.spec.server.pluginRegistryUrl = pluginRegistryUrl + cheClusterCR.spec.server.externalPluginRegistry = true + } + const devfileRegistryUrl = flags['devfile-registry-url'] + if (devfileRegistryUrl) { + cheClusterCR.spec.server.devfileRegistryUrl = devfileRegistryUrl + cheClusterCR.spec.server.externalDevfileRegistry = true + } + + cheClusterCR.spec.storage.postgresPVCStorageClassName = flags['postgres-pvc-storage-class-name'] + cheClusterCR.spec.storage.workspacePVCStorageClassName = flags['workspace-pvc-storage-class-name'] + + if (flags['workspace-engine'] === 'dev-workspace') { + cheClusterCR.spec.devWorkspace.enable = true + } + + // Use self-signed TLS certificate by default (for versions before 7.14.3). + // In modern versions of Che this field is ignored. + cheClusterCR.spec.server.selfSignedCert = true + } + + cheClusterCR.spec.server.cheClusterRoles = ctx.namespaceEditorClusterRoleName + + // override default values + if (ctx.crPatch) { + merge(cheClusterCR, ctx.crPatch) + } + + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.createNamespacedCustomObject('org.eclipse.che', 'v1', cheNamespace, 'checlusters', cheClusterCR) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async patchCheCluster(name: string, namespace: string, patch: any): Promise { + try { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + const { body } = await customObjectsApi.patchNamespacedCustomObject('org.eclipse.che', 'v1', namespace, 'checlusters', name, patch, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/merge-patch+json' } }) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + /** + * Returns `checlusters.org.eclipse.che' in the given namespace. + */ + async getCheCluster(cheNamespace: string): Promise { + return this.getCustomResource(cheNamespace, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) + } + + /** + * Returns custom resource in the given namespace. + */ + async getCustomResource(namespace: string, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.listNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural) + if (!(body as any).items) { + return + } + + const crs = (body as any).items as any[] + if (crs.length === 0) { + return + } else if (crs.length !== 1) { + throw new Error(`Too many resources of type ${resourcePlural}.${resourceAPIGroup} found in the namespace '${namespace}'`) + } + + return crs[0] + } catch (e) { + if (e.response && e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + /** + * Deletes `checlusters.org.eclipse.che' resources in the given namespace. + */ + async getAllCheClusters(): Promise { + return this.getAllCustomResources(CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) + } + + /** + * Returns all custom resources + */ + async getAllCustomResources(resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.listClusterCustomObject(resourceAPIGroup, resourceAPIVersion, resourcePlural) + return (body as any).items ? (body as any).items : [] + } catch (e) { + if (e.response.statusCode === 404) { + // There is no CRD + return [] + } + throw this.wrapK8sClientError(e) + } + } + + /** + * Deletes `checlusters.org.eclipse.che' resources in the given namespace. + */ + async deleteCheCluster(namespace: string): Promise { + return this.deleteCustomResource(namespace, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) + } + + /** + * Deletes custom resources in the given namespace. + */ + async deleteCustomResource(namespace: string, resourceAPIGroup: string, resourceAPIVersion: string, resourcePlural: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.listNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural) + if (!(body as any).items) { + return + } + + const crs = (body as any).items as any[] + for (const cr of crs) { + await customObjectsApi.deleteNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural, cr.metadata.name) + } + } catch (e) { + if (e.response.statusCode === 404) { + // There is no CRD + return + } + throw this.wrapK8sClientError(e) + } + } + + async isPreInstalledOLM(): Promise { + const apiApi = this.kubeConfig.makeApiClient(ApisApi) + try { + const { body } = await apiApi.getAPIVersions() + const OLMAPIGroup = body.groups.find(apiGroup => apiGroup.name === 'operators.coreos.com') + return Boolean(OLMAPIGroup) + } catch { + return false + } + } + + async getUsersNumber(): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + let amountOfUsers: number + try { + const { body } = await customObjectsApi.listClusterCustomObject('user.openshift.io', 'v1', 'users') + if (!(body as any).items) { + throw new Error('Unable to get list users.') + } + amountOfUsers = (body as any).items.length + } catch (e) { + throw this.wrapK8sClientError(e) + } + return amountOfUsers + } + + async getOpenshiftAuthProviders(): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + const oAuthName = 'cluster' + const { body } = await customObjectsApi.getClusterCustomObject('config.openshift.io', 'v1', 'oauths', oAuthName) + return (body as OAuth).spec.identityProviders + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async operatorSourceExists(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorsources', name) + return this.compare(body, name) + } catch { + return false + } + } + + async catalogSourceExists(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', name) + return this.compare(body, name) + } catch { + return false + } + } + + async getOAuthClientAuthorizations(clientName: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.listClusterCustomObject('oauth.openshift.io', 'v1', 'oauthclientauthorizations') + + if (!(body as any).items) { + return [] + } + const oauthClientAuthorizations = (body as any).items as any[] + return oauthClientAuthorizations.filter(o => o.clientName === clientName) + } catch (e) { + if (e.response.statusCode === 404) { + // There is no 'oauthclientauthorizations` + return [] + } + throw this.wrapK8sClientError(e) + } + } + + async deleteOAuthClientAuthorizations(oAuthClientAuthorizations: any[]): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const filetOauthAuthorizations = oAuthClientAuthorizations.filter((e => e.metadata && e.metadata.name)) + for (const oauthAuthorization of filetOauthAuthorizations) { + await customObjectsApi.deleteClusterCustomObject('oauth.openshift.io', 'v1', 'oauthclientauthorizations', oauthAuthorization.metadata.name) + } + } catch (e) { + if (e.response.statusCode === 404) { + return + } + throw this.wrapK8sClientError(e) + } + } + + async consoleLinkExists(name: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.getClusterCustomObject('console.openshift.io', 'v1', 'consolelinks', name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + // There are no consoleLink + return false + } + throw this.wrapK8sClientError(e) + } + } + + async deleteConsoleLink(name: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.deleteClusterCustomObject('console.openshift.io', 'v1', 'consolelinks', name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async getCatalogSource(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', name) + return body as CatalogSource + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + readCatalogSourceFromFile(filePath: string): CatalogSource { + const catalogSource = this.safeLoadFromYamlFile(filePath) as CatalogSource + if (!catalogSource.metadata || !catalogSource.metadata.name) { + throw new Error(`CatalogSource from ${filePath} must have specified metadata and name`) + } + return catalogSource + } + + async createCatalogSource(catalogSource: CatalogSource) { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const namespace = catalogSource.metadata.namespace! + const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', catalogSource) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async waitCatalogSource(namespace: string, catalogSourceName: string, timeout = 60): Promise { + return new Promise(async (resolve, reject) => { + const watcher = new Watch(this.kubeConfig) + const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/catalogsources`, + { fieldSelector: `metadata.name=${catalogSourceName}` }, + (_phase: string, obj: any) => { + resolve(obj as CatalogSource) + }, + error => { + if (error) { + reject(error) + } + }) + + setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${catalogSourceName}" catalog source is created.`) + }, timeout * 1000) + }) + } + + async deleteCatalogSource(namespace: string, catalogSourceName: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', catalogSourceName) + } catch (e) { + if (e.response.statusCode === 404) { + return + } + throw this.wrapK8sClientError(e) + } + } + + async operatorGroupExists(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', name) + return this.compare(body, name) + } catch { + return false + } + } + + async createOperatorGroup(operatorGroupName: string, namespace: string) { + const operatorGroup: OperatorGroup = { + apiVersion: 'operators.coreos.com/v1', + kind: 'OperatorGroup', + metadata: { + name: operatorGroupName, + namespace, + }, + spec: { + targetNamespaces: [namespace], + }, + } + + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', operatorGroup) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteOperatorGroup(operatorGroupName: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1', namespace, 'operatorgroups', operatorGroupName) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async createOperatorSubscription(subscription: Subscription) { + const namespace: string = subscription.metadata.namespace! + + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.createNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', subscription) + return body + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getOperatorSubscription(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', name) + return body as Subscription + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async operatorSubscriptionExists(name: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', name) + return this.compare(body, name) + } catch { + return false + } + } + + async deleteOperatorSubscription(operatorSubscriptionName: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', operatorSubscriptionName) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async waitOperatorSubscriptionReadyForApproval(namespace: string, subscriptionName: string, timeout = AWAIT_TIMEOUT_S): Promise { + return new Promise(async (resolve, reject) => { + const watcher = new Watch(this.kubeConfig) + const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/subscriptions`, + { fieldSelector: `metadata.name=${subscriptionName}` }, + (_phase: string, obj: any) => { + const subscription = obj as Subscription + if (subscription.status && subscription.status.conditions) { + for (const condition of subscription.status.conditions) { + if (condition.type === 'InstallPlanPending' && condition.status === 'True') { + resolve(subscription.status.installplan) + } + } + } + }, + error => { + if (error) { + reject(error) + } + }) + + setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${subscriptionName}" subscription is ready.`) + }, timeout * 1000) + }) + } + + async approveOperatorInstallationPlan(name = '', namespace = '') { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const patch: InstallPlan = { + spec: { + approved: true, + }, + } + await customObjectsApi.patchNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'installplans', name, patch, undefined, undefined, undefined, { headers: { 'Content-Type': 'application/merge-patch+json' } }) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async waitUntilOperatorIsInstalled(installPlanName: string, namespace: string, timeout = 240) { + return new Promise(async (resolve, reject) => { + const watcher = new Watch(this.kubeConfig) + const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/installplans`, + { fieldSelector: `metadata.name=${installPlanName}` }, + (_phase: string, obj: any) => { + const installPlan = obj as InstallPlan + if (installPlan.status && installPlan.status.phase === 'Failed') { + const errorMessage = [] + for (const condition of installPlan.status.conditions) { + if (!condition.reason) { + errorMessage.push(`Reason: ${condition.reason}`) + errorMessage.push(!condition.message ? `Message: ${condition.message}` : '') + } + } + reject(errorMessage.join(' ')) + } + if (installPlan.status && installPlan.status.conditions) { + for (const condition of installPlan.status.conditions) { + if (condition.type === 'Installed' && condition.status === 'True') { + resolve(installPlan) + } + } + } + }, + error => { + if (error) { + reject(error) + } + }) + + setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${installPlanName}" has go status 'Installed'.`) + }, timeout * 1000) + }) + } + + async getCSV(csvName: string, namespace: string): Promise { + const csvs = await this.getClusterServiceVersions(namespace) + return csvs.items.find(item => item.metadata.name === csvName) + } + + async getClusterServiceVersions(namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.listNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions') + return body as ClusterServiceVersionList + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async patchClusterServiceVersion(namespace: string, name: string, jsonPatch: any[]): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + const requestOptions = { + headers: { + 'content-type': 'application/json-patch+json', + }, + } + try { + const response = await customObjectsApi.patchNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions', name, jsonPatch, undefined, undefined, undefined, requestOptions) + return response.body as ClusterServiceVersion + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteClusterServiceVersion(namespace: string, csvName: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'clusterserviceversions', csvName) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async getPackageManifect(name: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + try { + const { body } = await customObjectsApi.getNamespacedCustomObject('packages.operators.coreos.com', 'v1', 'default', 'packagemanifests', name) + return body as PackageManifest + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async deleteNamespace(namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sCoreApi.deleteNamespace(namespace) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + /** + * Returns CRD version of Cert Manager + */ + async getCertManagerK8sApiVersion(): Promise { + return this.getCrdStorageVersion('certificates.cert-manager.io') + } + + async clusterIssuerExists(name: string, version: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + // If cluster issuers doesn't exist an exception will be thrown + await customObjectsApi.getClusterCustomObject('cert-manager.io', version, 'clusterissuers', name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async isNamespacedCertificateExists(name: string, version: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + // If cluster issuers doesn't exist an exception will be thrown + await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'certificates', name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async deleteNamespacedCertificate(name: string, version: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + // If cluster certificates doesn't exist an exception will be thrown + await customObjectsApi.deleteNamespacedCustomObject('cert-manager.io', version, namespace, 'certificates', name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async deleteNamespacedIssuer(name: string, version: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + await customObjectsApi.deleteNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', name) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async listClusterIssuers(version: string, labelSelector?: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + let res + try { + res = await customObjectsApi.listClusterCustomObject('cert-manager.io', version, 'clusterissuers', undefined, undefined, undefined, labelSelector) + } catch (e) { + throw this.wrapK8sClientError(e) + } + + if (!res || !res.body) { + throw new Error('Unable to get cluster issuers list') + } + const clusterIssuersList: { items?: any[] } = res.body + + return clusterIssuersList.items || [] + } + + async createCheClusterIssuer(cheClusterIssuerYamlPath: string, version: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + const cheClusterIssuer = this.safeLoadFromYamlFile(cheClusterIssuerYamlPath) + try { + await customObjectsApi.createClusterCustomObject('cert-manager.io', version, 'clusterissuers', cheClusterIssuer) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createCertificateIssuer(cheClusterIssuerYamlPath: string, version: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + const certificateIssuer = this.safeLoadFromYamlFile(cheClusterIssuerYamlPath) + try { + await customObjectsApi.createNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', certificateIssuer) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async isCertificateIssuerExists(name: string, version: string, namespace: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + // If issuers doesn't exist an exception will be thrown + await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', name) + return true + } catch (e) { + if (e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) + } + } + + async createCheClusterCertificate(certificate: V1Certificate, version: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + try { + await customObjectsApi.createNamespacedCustomObject('cert-manager.io', version, certificate.metadata.namespace, 'certificates', certificate) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async currentContext(): Promise { + return this.kubeConfig.getCurrentContext() + } + + getContext(name: string): Context | null { + return this.kubeConfig.getContextObject(name) + } + + /** + * Retrieve the default token from the default serviceAccount. + */ + async getDefaultServiceAccountToken(): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + const namespaceName = 'default' + const saName = 'default' + let res + // now get the matching secrets + try { + res = await k8sCoreApi.listNamespacedSecret(namespaceName) + } catch (e) { + throw this.wrapK8sClientError(e) + } + if (!res || !res.body) { + throw new Error('Unable to get default service account') + } + const v1SecretList = res.body + + if (!v1SecretList.items || v1SecretList.items.length === 0) { + throw new Error(`Unable to get default service account token since there is no secret in '${namespaceName}' namespace`) + } + + const v1DefaultSATokenSecret = v1SecretList.items.find(secret => secret.metadata!.annotations && + secret.metadata!.annotations['kubernetes.io/service-account.name'] === saName && + secret.type === 'kubernetes.io/service-account-token') + + if (!v1DefaultSATokenSecret) { + throw new Error(`Secret for '${saName}' service account is not found in namespace '${namespaceName}'`) + } + + return Buffer.from(v1DefaultSATokenSecret.data!.token, 'base64').toString() + } + + async checkKubeApi() { + const currentCluster = this.kubeConfig.getCurrentCluster() + if (!currentCluster) { + throw new Error(`The current context is unknown. It should be set using '${getClusterClientCommand()} config use-context ' or in another way.`) + } + + try { + await this.requestKubeHealthz(currentCluster) + } catch (error) { + if (error.message && (error.message as string).includes('E_K8S_API_UNAUTHORIZED')) { + const token = await this.getDefaultServiceAccountToken() + await this.requestKubeHealthz(currentCluster, token) + } else { + throw error + } + } + } + + async requestKubeHealthz(currentCluster: Cluster, token?: string) { + const endpoint = `${currentCluster.server}/healthz` + + try { + const config: AxiosRequestConfig = { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + requestCert: true, + }), + headers: token && { Authorization: 'bearer ' + token }, + } + + const response = await axios.get(`${endpoint}`, config) + if (!response || response.status !== 200 || response.data !== 'ok') { + throw new Error('E_BAD_RESP_K8S_API') + } + } catch (error) { + if (error.response && error.response.status === 403) { + throw new Error(`E_K8S_API_FORBIDDEN - Message: ${error.response.data.message}`) + } + if (error.response && error.response.status === 401) { + throw new Error(`E_K8S_API_UNAUTHORIZED - Message: ${error.response.data.message}`) + } + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + throw new Error(`E_K8S_API_UNKNOWN_ERROR - 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 + throw new Error(`E_K8S_API_NO_RESPONSE - Endpoint: ${endpoint} - Error message: ${error.message}`) + } else { + // Something happened in setting up the request that triggered an Error + throw new Error(`E_CHECTL_UNKNOWN_ERROR - Message: ${error.message}`) + } + } + } + + async isOpenShift(): Promise { + return this.IsAPIGroupSupported('apps.openshift.io') + } + + async isOpenShift3(): Promise { + const isAppsAPISupported = await this.IsAPIGroupSupported('apps.openshift.io') + const isConfigAPISupported = await this.IsAPIGroupSupported('config.openshift.io') + return isAppsAPISupported && !isConfigAPISupported + } + + async isOpenShift4(): Promise { + const isRouteAPISupported = await this.IsAPIGroupSupported('route.openshift.io') + const isConfigAPISupported = await this.IsAPIGroupSupported('config.openshift.io') + return isRouteAPISupported && isConfigAPISupported + } + + async IsAPIExtensionSupported(version: string): Promise { + return this.IsAPIGroupSupported('apiextensions.k8s.io', version) + } + + async IsAPIGroupSupported(name: string, version?: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(ApisApi) + try { + const res = await k8sCoreApi.getAPIVersions() + if (!res || !res.body || !res.body.groups) { + return false + } + + const group = res.body.groups.find(g => g.name === name) + if (!group) { + return false + } + + if (version) { + return Boolean(group.versions.find(v => v.version === version)) + } else { + return Boolean(group) + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getIngressHost(name = '', namespace = ''): Promise { + const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) + try { + const res = await k8sExtensionsApi.readNamespacedIngress(name, namespace) + if (res && res.body && + res.body.spec && + res.body.spec.rules && + res.body.spec.rules.length > 0) { + return res.body.spec.rules[0].host || '' + } + throw new Error('ERR_INGRESS_NO_HOST') + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getIngressProtocol(name = '', namespace = ''): Promise { + const k8sExtensionsApi = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) + try { + const res = await k8sExtensionsApi.readNamespacedIngress(name, namespace) + if (!res || !res.body || !res.body.spec) { + throw new Error('ERR_INGRESS_NO_HOST') + } + if (res.body.spec.tls && res.body.spec.tls.length > 0) { + return 'https' + } else { + return 'http' + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async getIngressesBySelector(labelSelector = '', namespace = ''): Promise { + const k8sV1Beta = this.kubeConfig.makeApiClient(ExtensionsV1beta1Api) + try { + const res = await k8sV1Beta.listNamespacedIngress(namespace, 'true', undefined, undefined, undefined, labelSelector) + if (res && res.body) { + return res.body + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + throw new Error('ERR_LIST_INGRESSES') + } + + async getSecret(name = '', namespace = 'default'): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + + // now get the matching secrets + try { + const res = await k8sCoreApi.readNamespacedSecret(name, namespace) + if (res && res.body && res.body) { + return res.body + } else { + return + } + } catch { + return + } + } + + /** + * Creates a secret with given name and data. + * Data should not be base64 encoded. + */ + async createSecret(name: string, data: { [key: string]: string }, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + + const secret = new V1Secret() + secret.metadata = new V1ObjectMeta() + secret.metadata.name = name + secret.metadata.namespace = namespace + secret.stringData = data + + try { + return (await k8sCoreApi.createNamespacedSecret(namespace, secret)).body + } catch { + return + } + } + + /** + * Awaits secret to be present and contain non-empty data fields specified in dataKeys parameter. + */ + async waitSecret(secretName: string, namespace: string, dataKeys: string[] = [], timeout = AWAIT_TIMEOUT_S): Promise { + return new Promise(async (resolve, reject) => { + // Set up watcher + const watcher = new Watch(this.kubeConfig) + const request = await watcher + .watch(`/api/v1/namespaces/${namespace}/secrets/`, { fieldSelector: `metadata.name=${secretName}` }, + (_phase: string, obj: any) => { + const secret = obj as V1Secret + + // Check all required data fields to be present + if (dataKeys.length > 0 && secret.data) { + for (const key of dataKeys) { + if (!secret.data[key]) { + // Key is missing or empty + return + } + } + } + + // The secret with all specified fields is present, stop watching + if (request) { + request.abort() + } + // Release awaiter + resolve() + }, + error => { + if (error) { + reject(error) + } + }) + + // Automatically stop watching after timeout + const timeoutHandler = setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for "${secretName}" secret.`) + }, timeout * 1000) + + // Request secret, for case if it is already exist + const secret = await this.getSecret(secretName, namespace) + if (secret) { + // Stop watching + request.abort() + clearTimeout(timeoutHandler) + + // Relese awaiter + resolve() + } + }) + } + + async persistentVolumeClaimExist(name = '', namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const { body } = await k8sCoreApi.readNamespacedPersistentVolumeClaim(name, namespace) + return this.compare(body, name) + } catch { + return false + } + } + + async deletePersistentVolumeClaim(name: string, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sCoreApi.deleteNamespacedPersistentVolumeClaim(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + + async getPersistentVolumeClaimsBySelector(labelSelector = '', namespace = ''): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + const res = await k8sCoreApi.listNamespacedPersistentVolumeClaim(namespace, 'true', undefined, undefined, undefined, labelSelector) + if (res && res.body) { + return res.body + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + throw new Error('ERR_LIST_PVCS') + } + + async listNamespace(): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + 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.kubeConfig.makeApiClient(CoreV1Api) + try { + const res = await k8sApi.listNamespacedPod(namespace, undefined, undefined, undefined, fieldSelector, labelSelector) + if (res && res.body) { + return res.body + } else { + return { + items: [], + } + } + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + /** + * Reads log by chunk and writes into a file. + */ + async readNamespacedPodLog(pod: string, namespace: string, container: string, filename: string, follow: boolean): Promise { + return new Promise(async (resolve, reject) => { + const logHelper = new Log(this.kubeConfig) + const stream = new Writable() + stream._write = function (chunk, encoding, done) { + fs.appendFileSync(filename, chunk, { encoding }) + done() + } + + await logHelper.log(namespace, pod, container, stream, error => { + stream.end() + if (error) { + reject(error) + } else { + resolve() + } + }, { follow }) + }) + } + + /** + * Forwards port, based on the example + * https://github.com/kubernetes-client/javascript/blob/master/examples/typescript/port-forward/port-forward.ts + */ + async portForward(podName: string, namespace: string, port: number): Promise { + const portForwardHelper = new PortForward(this.kubeConfig, true) + try { + const server = net.createServer(async socket => { + await portForwardHelper.portForward(namespace, podName, [port], socket, null, socket) + }) + server.listen(port, 'localhost') + return + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + /** + * Checks if message is present and returns error with it + * or returns error with the specified error if message is not found. + * + * @param e k8s error to wrap + */ + private wrapK8sClientError(e: any): Error { + if (e.response && e.response.body) { + return newError(e.response.body, e) + } + return e + } + + public safeLoadFromYamlFile(filePath: string): any { + return safeLoadFromYamlFile(filePath) + } } class PatchedK8sApi extends CoreV1Api { diff --git a/src/api/openshift.ts b/src/api/openshift.ts index 74d0b09e9..bf905787c 100644 --- a/src/api/openshift.ts +++ b/src/api/openshift.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -35,8 +35,9 @@ export class OpenShiftHelper { const termination = stdout.trim() if (termination && termination.includes('edge') || termination.includes('passthrough') || termination.includes('reencrypt')) { return 'https' + } else { + return 'http' } - return 'http' } async routeExist(name: string, namespace = ''): Promise { diff --git a/src/api/typings/cert-manager.d.ts b/src/api/typings/cert-manager.d.ts index eae8d420f..4669bf032 100644 --- a/src/api/typings/cert-manager.d.ts +++ b/src/api/typings/cert-manager.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/typings/olm.d.ts b/src/api/typings/olm.d.ts index 54677a1b5..9cf1c537b 100644 --- a/src/api/typings/olm.d.ts +++ b/src/api/typings/olm.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/typings/openshift.d.ts b/src/api/typings/openshift.d.ts index f062081ac..853ea1790 100644 --- a/src/api/typings/openshift.d.ts +++ b/src/api/typings/openshift.d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/api/version.ts b/src/api/version.ts index 184db09ea..f760d820e 100644 --- a/src/api/version.ts +++ b/src/api/version.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -200,7 +200,7 @@ export namespace VersionHelper { const { data } = await axiosInstance.get(`https://che-incubator.github.io/chectl/channels/${channel}/linux-x64`) return data.version } catch { - + return } } diff --git a/src/commands/auth/delete.ts b/src/commands/auth/delete.ts index 2eb673711..5651616e4 100644 --- a/src/commands/auth/delete.ts +++ b/src/commands/auth/delete.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/auth/get.ts b/src/commands/auth/get.ts index ff04956ac..0a4baa5ad 100644 --- a/src/commands/auth/get.ts +++ b/src/commands/auth/get.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/auth/list.ts b/src/commands/auth/list.ts index fdfb16fb9..719b934d5 100644 --- a/src/commands/auth/list.ts +++ b/src/commands/auth/list.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index daf26f830..6d6373def 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts index 4ecce90fb..c1011d7c8 100644 --- a/src/commands/auth/logout.ts +++ b/src/commands/auth/logout.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/auth/use.ts b/src/commands/auth/use.ts index 4dbe299ab..aff51d730 100644 --- a/src/commands/auth/use.ts +++ b/src/commands/auth/use.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -130,7 +130,7 @@ export default class Use extends Command { if (allLogins.size === 0) { cli.info('No login session exists') return - } if (allLogins.size === 1) { + } else if (allLogins.size === 1) { // Retrieve the only login info cheApiEndpoint = allLogins.keys().next().value username = allLogins.get(cheApiEndpoint)![0] diff --git a/src/commands/cacert/export.ts b/src/commands/cacert/export.ts index 2f0d210ea..d2fe02732 100644 --- a/src/commands/cacert/export.ts +++ b/src/commands/cacert/export.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/dashboard/open.ts b/src/commands/dashboard/open.ts index 012037071..fba10b9b8 100644 --- a/src/commands/dashboard/open.ts +++ b/src/commands/dashboard/open.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/devfile/generate.ts b/src/commands/devfile/generate.ts index 79cb45ee5..87c62dc50 100644 --- a/src/commands/devfile/generate.ts +++ b/src/commands/devfile/generate.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -251,8 +251,9 @@ export default class Generate extends Command { const updatedArgs = process.argv.slice(index).map(arg => { if (arg.indexOf(' ') >= 0) { return arg.replace(/(.*?)=(.*)/g, '$1=\"$2\"') + } else { + return arg } - return arg }) this.log(`# chectl ${updatedArgs.join(' ')}`) this.log(yaml.safeDump(devfile)) diff --git a/src/commands/server/debug.ts b/src/commands/server/debug.ts index 932a1da8a..5460fab1d 100644 --- a/src/commands/server/debug.ts +++ b/src/commands/server/debug.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/server/delete.ts b/src/commands/server/delete.ts index 5f1e8aa2e..a8c682ce8 100644 --- a/src/commands/server/delete.ts +++ b/src/commands/server/delete.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/server/deploy.ts b/src/commands/server/deploy.ts index fd447ecf1..32802679c 100644 --- a/src/commands/server/deploy.ts +++ b/src/commands/server/deploy.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -29,447 +29,447 @@ import { PlatformTasks } from '../../tasks/platforms/platform' import { askForChectlUpdateIfNeeded, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, getProjectName, isKubernetesPlatformFamily, isOpenshiftPlatformFamily, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' export default class Deploy extends Command { - static description = 'Deploy Eclipse Che server' - - static flags: flags.Input = { - help: flags.help({ char: 'h' }), - chenamespace: cheNamespace, - batch, - 'listr-renderer': listrRenderer, - 'deployment-name': cheDeployment, - cheimage: string({ - char: 'i', - description: 'Eclipse Che server container image', - env: 'CHE_CONTAINER_IMAGE', - }), - templates: string({ - char: 't', - description: 'Path to the templates folder', - env: 'CHE_TEMPLATES_FOLDER', - exclusive: [DEPLOY_VERSION_KEY], - }), - 'devfile-registry-url': string({ - description: 'The URL of the external Devfile registry.', - env: 'CHE_WORKSPACE_DEVFILE__REGISTRY__URL', - }), - 'plugin-registry-url': string({ - description: 'The URL of the external plugin registry.', - env: 'CHE_WORKSPACE_PLUGIN__REGISTRY__URL', - }), - cheboottimeout: string({ - char: 'o', - description: 'Eclipse Che server bootstrap timeout (in milliseconds)', - default: '40000', - required: true, - env: 'CHE_SERVER_BOOT_TIMEOUT', - }), - [K8SPODWAITTIMEOUT_KEY]: k8sPodWaitTimeout, - [K8SPODREADYTIMEOUT_KEY]: k8sPodReadyTimeout, - [K8SPODDOWNLOADIMAGETIMEOUT_KEY]: k8sPodDownloadImageTimeout, - [K8SPODERRORRECHECKTIMEOUT_KEY]: k8sPodErrorRecheckTimeout, - [LOG_DIRECTORY_KEY]: logsDirectory, - multiuser: flags.boolean({ - char: 'm', - description: 'Starts Eclipse Che in multi-user mode', - default: false, - }), - tls: flags.boolean({ - char: 's', - description: `Deprecated. Enable TLS encryption. - Note, this option is turned on by default. - To provide own certificate for Kubernetes infrastructure, 'che-tls' secret with TLS certificate must be pre-created in the configured namespace. - In case of providing own self-signed certificate 'self-signed-certificate' secret should be also created. - For OpenShift, router will use default cluster certificates. - Please see the docs how to deploy Eclipse Che on different infrastructures: ${DOCS_LINK_INSTALL_RUNNING_CHE_LOCALLY}`, - hidden: true, - }), - 'self-signed-cert': flags.boolean({ - description: 'Deprecated. The flag is ignored. Self signed certificates usage is autodetected now.', - default: false, - hidden: true, - }), - platform: string({ - char: 'p', - description: 'Type of Kubernetes platform. Valid values are \"minikube\", \"minishift\", \"k8s (for kubernetes)\", \"openshift\", \"crc (for CodeReady Containers)\", \"microk8s\".', - options: ['minikube', 'minishift', 'k8s', 'openshift', 'microk8s', 'docker-desktop', 'crc'], - }), - installer: string({ - char: 'a', - description: 'Installer type. If not set, default is "olm" for OpenShift 4.x platform otherwise "operator".', - options: ['helm', 'operator', 'olm'], - }), - domain: string({ - char: 'b', - description: `Domain of the Kubernetes cluster (e.g. example.k8s-cluster.com or .nip.io) - This flag makes sense only for Kubernetes family infrastructures and will be autodetected for Minikube and MicroK8s in most cases. - However, for Kubernetes cluster it is required to specify. - Please note, that just setting this flag will not likely work out of the box. - According changes should be done in Kubernetes cluster configuration as well. - In case of Openshift, domain adjustment should be done on the cluster configuration level.`, - default: '', - }), - debug: boolean({ - description: 'Enables the debug mode for Eclipse Che server. To debug Eclipse Che server from localhost use \'server:debug\' command.', - default: false, - }), - 'che-operator-image': string({ - description: 'Container image of the operator. This parameter is used only when the installer is the operator', - }), - [CHE_OPERATOR_CR_YAML_KEY]: cheOperatorCRYaml, - [CHE_OPERATOR_CR_PATCH_YAML_KEY]: cheOperatorCRPatchYaml, - 'helm-patch-yaml': string({ - description: `Path to yaml file with Helm Chart values patch. - The file format is identical to values.yaml from the chart. - Note, Provided command line arguments take precedence over patch file.`, - default: '', - }), - 'workspace-pvc-storage-class-name': string({ - description: 'persistent volume(s) storage class name to use to store Eclipse Che workspaces data', - env: 'CHE_INFRA_KUBERNETES_PVC_STORAGE__CLASS__NAME', - default: '', - }), - 'postgres-pvc-storage-class-name': string({ - description: 'persistent volume storage class name to use to store Eclipse Che postgres database', - default: '', - }), - 'skip-version-check': flags.boolean({ - description: 'Skip minimal versions check.', - default: false, - }), - 'skip-cluster-availability-check': flags.boolean({ - description: 'Skip cluster availability check. The check is a simple request to ensure the cluster is reachable.', - default: false, - }), - 'auto-update': flags.boolean({ - description: `Auto update approval strategy for installation Eclipse Che. - With this strategy will be provided auto-update Eclipse Che without any human interaction. - By default this flag is enabled. - This parameter is used only when the installer is 'olm'.`, - allowNo: true, - exclusive: ['starting-csv'], - }), - 'starting-csv': flags.string({ - description: `Starting cluster service version(CSV) for installation Eclipse Che. - Flags uses to set up start installation version Che. - For example: 'starting-csv' provided with value 'eclipse-che.v7.10.0' for stable channel. - Then OLM will install Eclipse Che with version 7.10.0. - Notice: this flag will be ignored with 'auto-update' flag. OLM with auto-update mode installs the latest known version. - This parameter is used only when the installer is 'olm'.`, - }), - 'olm-channel': string({ - description: `Olm channel to install Eclipse Che, f.e. stable. - If options was not set, will be used default version for package manifest. - This parameter is used only when the installer is the 'olm'.`, - }), - 'package-manifest-name': string({ - description: `Package manifest name to subscribe to Eclipse Che OLM package manifest. - This parameter is used only when the installer is the 'olm'.`, - }), - 'catalog-source-yaml': string({ - description: `Path to a yaml file that describes custom catalog source for installation Eclipse Che operator. - Catalog source will be applied to the namespace with Che operator. - Also you need define 'olm-channel' name and 'package-manifest-name'. - This parameter is used only when the installer is the 'olm'.`, - }), - 'catalog-source-name': string({ - description: `OLM catalog source to install Eclipse Che operator. - This parameter is used only when the installer is the 'olm'.`, - }), - 'catalog-source-namespace': string({ - description: `Namespace for OLM catalog source to install Eclipse Che operator. - This parameter is used only when the installer is the 'olm'.`, - }), - 'cluster-monitoring': boolean({ - default: false, - hidden: true, - description: `Enable cluster monitoring to scrape Eclipse Che metrics in Prometheus. - This parameter is used only when the platform is 'openshift'.`, - }), - 'olm-suggested-namespace': boolean({ - default: true, - allowNo: true, - description: `Indicate to deploy Eclipse Che in OLM suggested namespace: '${DEFAULT_OLM_SUGGESTED_NAMESPACE}'. - Flag 'chenamespace' is ignored in this case - This parameter is used only when the installer is 'olm'.`, - }), - 'skip-kubernetes-health-check': skipK8sHealthCheck, - 'workspace-engine': string({ - description: 'Workspace Engine. If not set, default is "che-server". "dev-workspace" is experimental.', - options: ['che-server', 'dev-workspace'], - default: 'che-server', - }), - 'dev-workspace-controller-namespace': devWorkspaceControllerNamespace, - telemetry: CHE_TELEMETRY, - [DEPLOY_VERSION_KEY]: cheDeployVersion, - } - - async setPlaformDefaults(flags: any, ctx: any): Promise { - flags.tls = await this.checkTlsMode(ctx) - if (flags['self-signed-cert']) { - this.warn('"self-signed-cert" flag is deprecated and has no effect. Autodetection is used instead.') - } - - if (!flags.installer) { - await this.setDefaultInstaller(flags, ctx) - cli.info(`โ€บ Installer type is set to: '${flags.installer}'`) - } - - if (flags.installer === 'olm' && flags['olm-suggested-namespace']) { - flags.chenamespace = DEFAULT_OLM_SUGGESTED_NAMESPACE - cli.info(` โ•olm-suggested-namespace flag is turned on. Eclipse Che will be deployed in namespace: ${DEFAULT_OLM_SUGGESTED_NAMESPACE}.`) - } - - if (!ctx.isChectl && flags.version) { - // Flavors of chectl should not use upstream repositories, so version flag is not applicable - this.error(`${getProjectName()} does not support '--version' flag.`) - } - if (!flags.templates && !flags.version) { - // Use build-in templates if no custom templates nor version to deploy specified. - // All flavors should use embedded templates if not custom templates is given. - flags.templates = getEmbeddedTemplatesDirectory() - } - } - - /** - * Checks if TLS is disabled via operator custom resource. - * Returns true if TLS is enabled (or omitted) and false if it is explicitly disabled. - */ - async checkTlsMode(ctx: any): Promise { - const crPatch = ctx.crPatch - if (crPatch && crPatch.spec && crPatch.spec.server && crPatch.spec.server.tlsSupport === false) { - return false - } - - const customCR = ctx.customCR - if (customCR && customCR.spec && customCR.spec.server && customCR.spec.server.tlsSupport === false) { - return false - } - - return true - } - - private isDevWorkspaceEnabled(ctx: any): boolean { - const crPatch = ctx.crPatch - if (crPatch && crPatch.spec && crPatch.spec.devWorkspace && crPatch.spec.devWorkspace.enable) { - return true - } - - const customCR = ctx.customCR - if (customCR && customCR.spec && customCR.spec.devWorkspace && customCR.spec.devWorkspace.enable) { - return true - } - - return false - } - - private checkCompatibility(flags: any) { - if (flags.installer === 'operator' && flags[CHE_OPERATOR_CR_YAML_KEY]) { - const ignoredFlags = [] - flags['plugin-registry-url'] && ignoredFlags.push('--plugin-registry-url') - flags['devfile-registry-url'] && ignoredFlags.push('--devfile-registry-url') - flags['postgres-pvc-storage-class-name'] && ignoredFlags.push('--postgres-pvc-storage-class-name') - flags['workspace-pvc-storage-class-name'] && ignoredFlags.push('--workspace-pvc-storage-class-name') - flags.tls && ignoredFlags.push('--tls') - flags.cheimage && ignoredFlags.push('--cheimage') - flags.debug && ignoredFlags.push('--debug') - flags.domain && ignoredFlags.push('--domain') - flags.multiuser && ignoredFlags.push('--multiuser') - - if (ignoredFlags.length) { - this.warn(`--${CHE_OPERATOR_CR_YAML_KEY} is used. The following flag(s) will be ignored: ${ignoredFlags.join('\t')}`) - } - } - - if (flags.domain && !flags[CHE_OPERATOR_CR_YAML_KEY] && isOpenshiftPlatformFamily(flags.platform)) { - this.warn('"--domain" flag is ignored for Openshift family infrastructures. It should be done on the cluster level.') - } - - if (flags.installer === 'helm') { - if (!isKubernetesPlatformFamily(flags.platform) && flags.platform !== 'docker-desktop') { - this.error(`๐Ÿ›‘ Current platform is ${flags.platform}. Helm installer is only available on top of Kubernetes flavor platform (including Minikube, Docker Desktop).`) - } - } - - if (flags.installer === 'olm') { - // OLM installer only checks - if (flags.platform === 'minishift') { - this.error(`๐Ÿ›‘ The specified installer ${flags.installer} does not support Minishift`) - } - - if (flags['catalog-source-name'] && flags['catalog-source-yaml']) { - this.error('should be provided only one argument: "catalog-source-name" or "catalog-source-yaml"') - } - if (flags.version) { - if (flags['starting-csv']) { - this.error('"starting-csv" and "version" flags are mutually exclusive. Please specify only one of them.') - } - if (flags['olm-channel']) { - this.error('"starting-csv" and "version" flags are mutually exclusive. Use "starting-csv" with "olm-channel" flag.') - } - if (flags['auto-update']) { - this.error('enabled "auto-update" flag cannot be used with version flag. Deploy latest version instead.') - } - } - - if (!flags['package-manifest-name'] && flags['catalog-source-yaml']) { - this.error('you need to define "package-manifest-name" flag to use "catalog-source-yaml".') - } - if (!flags['olm-channel'] && flags['catalog-source-yaml']) { - this.error('you need to define "olm-channel" flag to use "catalog-source-yaml".') - } - } else { - // Not OLM installer - if (flags['starting-csv']) { - this.error('"starting-csv" flag should be used only with "olm" installer.') - } - if (flags['catalog-source-yaml']) { - this.error('"catalog-source-yaml" flag should be used only with "olm" installer.') - } - if (flags['olm-channel']) { - this.error('"olm-channel" flag should be used only with "olm" installer.') - } - if (flags['package-manifest-name']) { - this.error('"package-manifest-name" flag should be used only with "olm" installer.') - } - if (flags['catalog-source-name']) { - this.error('"catalog-source-name" flag should be used only with "olm" installer.') - } - if (flags['catalog-source-namespace']) { - this.error('"package-manifest-name" flag should be used only with "olm" installer.') - } - if (flags['cluster-monitoring'] && flags.platform !== 'openshift') { - this.error('"cluster-monitoring" flag should be used only with "olm" installer and "openshift" platform.') - } - } - - if (flags.version) { - // Check minimal allowed version to install - let minAllowedVersion: string - switch (flags.installer) { - case 'olm': - minAllowedVersion = MIN_OLM_INSTALLER_VERSION - break - case 'operator': - minAllowedVersion = MIN_CHE_OPERATOR_INSTALLER_VERSION - break - case 'helm': - minAllowedVersion = MIN_HELM_INSTALLER_VERSION - break - default: - // Should never happen - minAllowedVersion = 'latest' - } - - let isVersionAllowed = false - try { - isVersionAllowed = semver.gte(flags.version, minAllowedVersion) - } catch (error) { - // not to fail unexpectedly - cli.debug(`Failed to compare versions '${flags.version}' and '${minAllowedVersion}': ${error}`) - } - - if (!isVersionAllowed) { - throw new Error(`This chectl version can deploy version ${minAllowedVersion} and higher. If you need to deploy ${flags.version} or lower, download the corresponding legacy chectl version.`) - } - } - } - - async run() { - const { flags } = this.parse(Deploy) - flags.chenamespace = flags.chenamespace || DEFAULT_CHE_NAMESPACE - const ctx = await ChectlContext.initAndGet(flags, this) - - if (!flags.batch && ctx.isChectl) { - await askForChectlUpdateIfNeeded() - } - - await this.setPlaformDefaults(flags, ctx) - await this.config.runHook(DEFAULT_ANALYTIC_HOOK_NAME, { command: Deploy.id, flags }) - - const cheTasks = new CheTasks(flags) - const platformTasks = new PlatformTasks() - const installerTasks = new InstallerTasks() - const apiTasks = new ApiTasks() - const devWorkspaceTasks = new DevWorkspaceTasks(flags) - - // Platform Checks - const platformCheckTasks = new Listr(platformTasks.preflightCheckTasks(flags, this), ctx.listrOptions) - - // Checks if Eclipse Che is already deployed - const preInstallTasks = new Listr(undefined, ctx.listrOptions) - preInstallTasks.add(apiTasks.testApiTasks(flags)) - preInstallTasks.add({ - title: '๐Ÿ‘€ Looking for an already existing Eclipse Che instance', - task: () => new Listr(cheTasks.checkIfCheIsInstalledTasks(flags)), - }) - preInstallTasks.add(checkChectlAndCheVersionCompatibility(flags)) - preInstallTasks.add(downloadTemplates(flags)) - preInstallTasks.add({ - title: '๐Ÿงช DevWorkspace engine (experimental / technology preview) ๐Ÿšจ', - enabled: () => (this.isDevWorkspaceEnabled(ctx) || flags['workspace-engine'] === 'dev-workspace') && !ctx.isOpenShift, - task: () => new Listr(devWorkspaceTasks.getInstallTasks(flags)), - }) - const installTasks = new Listr(installerTasks.installTasks(flags, this), ctx.listrOptions) - - // Post Install Checks - const postInstallTasks = new Listr([ - { - title: 'โœ… Post installation checklist', - task: () => new Listr(cheTasks.waitDeployedChe()), - }, - getRetrieveKeycloakCredentialsTask(flags), - retrieveCheCaCertificateTask(flags), - ...cheTasks.preparePostInstallationOutput(flags), - getPrintHighlightedMessagesTask(), - ], ctx.listrOptions) - - const logsTasks = new Listr([{ - title: 'Following Eclipse Che logs', - task: () => new Listr(cheTasks.serverLogsTasks(flags, true)), - }], ctx.listrOptions) - - try { - await preInstallTasks.run(ctx) - - if (ctx.isCheDeployed) { - let message = 'Eclipse Che has been already deployed.' - if (!ctx.isCheReady) { - message += ' Use server:start command to start a stopped Eclipse Che instance.' - } - cli.warn(message) - } else { - this.checkCompatibility(flags) - await platformCheckTasks.run(ctx) - await logsTasks.run(ctx) - await installTasks.run(ctx) - await postInstallTasks.run(ctx) - this.log(getCommandSuccessMessage()) - } - } catch (err) { - this.error(wrapCommandError(err)) - } - - notifyCommandCompletedSuccessfully() - this.exit(0) - } - - /** - * Sets default installer which is `olm` for OpenShift 4 with stable version of chectl - * and `operator` for other cases. - */ - async setDefaultInstaller(flags: any, _ctx: any): Promise { - const kubeHelper = new KubeHelper(flags) - - const isOlmPreinstalled = await kubeHelper.isPreInstalledOLM() - if ((flags['catalog-source-name'] || flags['catalog-source-yaml']) && isOlmPreinstalled) { - flags.installer = 'olm' - return - } - - if (flags.platform === 'openshift' && await kubeHelper.isOpenShift4() && isOlmPreinstalled) { - flags.installer = 'olm' - } else { - flags.installer = 'operator' - } - } + static description = 'Deploy Eclipse Che server' + + static flags: flags.Input = { + help: flags.help({ char: 'h' }), + chenamespace: cheNamespace, + batch, + 'listr-renderer': listrRenderer, + 'deployment-name': cheDeployment, + cheimage: string({ + char: 'i', + description: 'Eclipse Che server container image', + env: 'CHE_CONTAINER_IMAGE', + }), + templates: string({ + char: 't', + description: 'Path to the templates folder', + env: 'CHE_TEMPLATES_FOLDER', + exclusive: [DEPLOY_VERSION_KEY], + }), + 'devfile-registry-url': string({ + description: 'The URL of the external Devfile registry.', + env: 'CHE_WORKSPACE_DEVFILE__REGISTRY__URL', + }), + 'plugin-registry-url': string({ + description: 'The URL of the external plugin registry.', + env: 'CHE_WORKSPACE_PLUGIN__REGISTRY__URL', + }), + cheboottimeout: string({ + char: 'o', + description: 'Eclipse Che server bootstrap timeout (in milliseconds)', + default: '40000', + required: true, + env: 'CHE_SERVER_BOOT_TIMEOUT', + }), + [K8SPODWAITTIMEOUT_KEY]: k8sPodWaitTimeout, + [K8SPODREADYTIMEOUT_KEY]: k8sPodReadyTimeout, + [K8SPODDOWNLOADIMAGETIMEOUT_KEY]: k8sPodDownloadImageTimeout, + [K8SPODERRORRECHECKTIMEOUT_KEY]: k8sPodErrorRecheckTimeout, + [LOG_DIRECTORY_KEY]: logsDirectory, + multiuser: flags.boolean({ + char: 'm', + description: 'Starts Eclipse Che in multi-user mode', + default: false, + }), + tls: flags.boolean({ + char: 's', + description: `Deprecated. Enable TLS encryption. + Note, this option is turned on by default. + To provide own certificate for Kubernetes infrastructure, 'che-tls' secret with TLS certificate must be pre-created in the configured namespace. + In case of providing own self-signed certificate 'self-signed-certificate' secret should be also created. + For OpenShift, router will use default cluster certificates. + Please see the docs how to deploy Eclipse Che on different infrastructures: ${DOCS_LINK_INSTALL_RUNNING_CHE_LOCALLY}`, + hidden: true, + }), + 'self-signed-cert': flags.boolean({ + description: 'Deprecated. The flag is ignored. Self signed certificates usage is autodetected now.', + default: false, + hidden: true, + }), + platform: string({ + char: 'p', + description: 'Type of Kubernetes platform. Valid values are \"minikube\", \"minishift\", \"k8s (for kubernetes)\", \"openshift\", \"crc (for CodeReady Containers)\", \"microk8s\".', + options: ['minikube', 'minishift', 'k8s', 'openshift', 'microk8s', 'docker-desktop', 'crc'], + }), + installer: string({ + char: 'a', + description: 'Installer type. If not set, default is "olm" for OpenShift 4.x platform otherwise "operator".', + options: ['helm', 'operator', 'olm'], + }), + domain: string({ + char: 'b', + description: `Domain of the Kubernetes cluster (e.g. example.k8s-cluster.com or .nip.io) + This flag makes sense only for Kubernetes family infrastructures and will be autodetected for Minikube and MicroK8s in most cases. + However, for Kubernetes cluster it is required to specify. + Please note, that just setting this flag will not likely work out of the box. + According changes should be done in Kubernetes cluster configuration as well. + In case of Openshift, domain adjustment should be done on the cluster configuration level.`, + default: '', + }), + debug: boolean({ + description: 'Enables the debug mode for Eclipse Che server. To debug Eclipse Che server from localhost use \'server:debug\' command.', + default: false, + }), + 'che-operator-image': string({ + description: 'Container image of the operator. This parameter is used only when the installer is the operator', + }), + [CHE_OPERATOR_CR_YAML_KEY]: cheOperatorCRYaml, + [CHE_OPERATOR_CR_PATCH_YAML_KEY]: cheOperatorCRPatchYaml, + 'helm-patch-yaml': string({ + description: `Path to yaml file with Helm Chart values patch. + The file format is identical to values.yaml from the chart. + Note, Provided command line arguments take precedence over patch file.`, + default: '', + }), + 'workspace-pvc-storage-class-name': string({ + description: 'persistent volume(s) storage class name to use to store Eclipse Che workspaces data', + env: 'CHE_INFRA_KUBERNETES_PVC_STORAGE__CLASS__NAME', + default: '', + }), + 'postgres-pvc-storage-class-name': string({ + description: 'persistent volume storage class name to use to store Eclipse Che postgres database', + default: '', + }), + 'skip-version-check': flags.boolean({ + description: 'Skip minimal versions check.', + default: false, + }), + 'skip-cluster-availability-check': flags.boolean({ + description: 'Skip cluster availability check. The check is a simple request to ensure the cluster is reachable.', + default: false, + }), + 'auto-update': flags.boolean({ + description: `Auto update approval strategy for installation Eclipse Che. + With this strategy will be provided auto-update Eclipse Che without any human interaction. + By default this flag is enabled. + This parameter is used only when the installer is 'olm'.`, + allowNo: true, + exclusive: ['starting-csv'], + }), + 'starting-csv': flags.string({ + description: `Starting cluster service version(CSV) for installation Eclipse Che. + Flags uses to set up start installation version Che. + For example: 'starting-csv' provided with value 'eclipse-che.v7.10.0' for stable channel. + Then OLM will install Eclipse Che with version 7.10.0. + Notice: this flag will be ignored with 'auto-update' flag. OLM with auto-update mode installs the latest known version. + This parameter is used only when the installer is 'olm'.`, + }), + 'olm-channel': string({ + description: `Olm channel to install Eclipse Che, f.e. stable. + If options was not set, will be used default version for package manifest. + This parameter is used only when the installer is the 'olm'.`, + }), + 'package-manifest-name': string({ + description: `Package manifest name to subscribe to Eclipse Che OLM package manifest. + This parameter is used only when the installer is the 'olm'.`, + }), + 'catalog-source-yaml': string({ + description: `Path to a yaml file that describes custom catalog source for installation Eclipse Che operator. + Catalog source will be applied to the namespace with Che operator. + Also you need define 'olm-channel' name and 'package-manifest-name'. + This parameter is used only when the installer is the 'olm'.`, + }), + 'catalog-source-name': string({ + description: `OLM catalog source to install Eclipse Che operator. + This parameter is used only when the installer is the 'olm'.`, + }), + 'catalog-source-namespace': string({ + description: `Namespace for OLM catalog source to install Eclipse Che operator. + This parameter is used only when the installer is the 'olm'.`, + }), + 'cluster-monitoring': boolean({ + default: false, + hidden: true, + description: `Enable cluster monitoring to scrape Eclipse Che metrics in Prometheus. + This parameter is used only when the platform is 'openshift'.`, + }), + 'olm-suggested-namespace': boolean({ + default: true, + allowNo: true, + description: `Indicate to deploy Eclipse Che in OLM suggested namespace: '${DEFAULT_OLM_SUGGESTED_NAMESPACE}'. + Flag 'chenamespace' is ignored in this case + This parameter is used only when the installer is 'olm'.`, + }), + 'skip-kubernetes-health-check': skipK8sHealthCheck, + 'workspace-engine': string({ + description: 'Workspace Engine. If not set, default is "che-server". "dev-workspace" is experimental.', + options: ['che-server', 'dev-workspace'], + default: 'che-server', + }), + 'dev-workspace-controller-namespace': devWorkspaceControllerNamespace, + telemetry: CHE_TELEMETRY, + [DEPLOY_VERSION_KEY]: cheDeployVersion, + } + + async setPlaformDefaults(flags: any, ctx: any): Promise { + flags.tls = await this.checkTlsMode(ctx) + if (flags['self-signed-cert']) { + this.warn('"self-signed-cert" flag is deprecated and has no effect. Autodetection is used instead.') + } + + if (!flags.installer) { + await this.setDefaultInstaller(flags, ctx) + cli.info(`โ€บ Installer type is set to: '${flags.installer}'`) + } + + if (flags.installer === 'olm' && flags['olm-suggested-namespace']) { + flags.chenamespace = DEFAULT_OLM_SUGGESTED_NAMESPACE + cli.info(` โ•olm-suggested-namespace flag is turned on. Eclipse Che will be deployed in namespace: ${DEFAULT_OLM_SUGGESTED_NAMESPACE}.`) + } + + if (!ctx.isChectl && flags.version) { + // Flavors of chectl should not use upstream repositories, so version flag is not applicable + this.error(`${getProjectName()} does not support '--version' flag.`) + } + if (!flags.templates && !flags.version) { + // Use build-in templates if no custom templates nor version to deploy specified. + // All flavors should use embedded templates if not custom templates is given. + flags.templates = getEmbeddedTemplatesDirectory() + } + } + + /** + * Checks if TLS is disabled via operator custom resource. + * Returns true if TLS is enabled (or omitted) and false if it is explicitly disabled. + */ + async checkTlsMode(ctx: any): Promise { + const crPatch = ctx.crPatch + if (crPatch && crPatch.spec && crPatch.spec.server && crPatch.spec.server.tlsSupport === false) { + return false + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.server && customCR.spec.server.tlsSupport === false) { + return false + } + + return true + } + + private isDevWorkspaceEnabled(ctx: any): boolean { + const crPatch = ctx.crPatch + if (crPatch && crPatch.spec && crPatch.spec.devWorkspace && crPatch.spec.devWorkspace.enable) { + return true + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.devWorkspace && customCR.spec.devWorkspace.enable) { + return true + } + + return false + } + + private checkCompatibility(flags: any) { + if (flags.installer === 'operator' && flags[CHE_OPERATOR_CR_YAML_KEY]) { + const ignoredFlags = [] + flags['plugin-registry-url'] && ignoredFlags.push('--plugin-registry-url') + flags['devfile-registry-url'] && ignoredFlags.push('--devfile-registry-url') + flags['postgres-pvc-storage-class-name'] && ignoredFlags.push('--postgres-pvc-storage-class-name') + flags['workspace-pvc-storage-class-name'] && ignoredFlags.push('--workspace-pvc-storage-class-name') + flags.tls && ignoredFlags.push('--tls') + flags.cheimage && ignoredFlags.push('--cheimage') + flags.debug && ignoredFlags.push('--debug') + flags.domain && ignoredFlags.push('--domain') + flags.multiuser && ignoredFlags.push('--multiuser') + + if (ignoredFlags.length) { + this.warn(`--${CHE_OPERATOR_CR_YAML_KEY} is used. The following flag(s) will be ignored: ${ignoredFlags.join('\t')}`) + } + } + + if (flags.domain && !flags[CHE_OPERATOR_CR_YAML_KEY] && isOpenshiftPlatformFamily(flags.platform)) { + this.warn('"--domain" flag is ignored for Openshift family infrastructures. It should be done on the cluster level.') + } + + if (flags.installer === 'helm') { + if (!isKubernetesPlatformFamily(flags.platform) && flags.platform !== 'docker-desktop') { + this.error(`๐Ÿ›‘ Current platform is ${flags.platform}. Helm installer is only available on top of Kubernetes flavor platform (including Minikube, Docker Desktop).`) + } + } + + if (flags.installer === 'olm') { + // OLM installer only checks + if (flags.platform === 'minishift') { + this.error(`๐Ÿ›‘ The specified installer ${flags.installer} does not support Minishift`) + } + + if (flags['catalog-source-name'] && flags['catalog-source-yaml']) { + this.error('should be provided only one argument: "catalog-source-name" or "catalog-source-yaml"') + } + if (flags.version) { + if (flags['starting-csv']) { + this.error('"starting-csv" and "version" flags are mutually exclusive. Please specify only one of them.') + } + if (flags['olm-channel']) { + this.error('"starting-csv" and "version" flags are mutually exclusive. Use "starting-csv" with "olm-channel" flag.') + } + if (flags['auto-update']) { + this.error('enabled "auto-update" flag cannot be used with version flag. Deploy latest version instead.') + } + } + + if (!flags['package-manifest-name'] && flags['catalog-source-yaml']) { + this.error('you need to define "package-manifest-name" flag to use "catalog-source-yaml".') + } + if (!flags['olm-channel'] && flags['catalog-source-yaml']) { + this.error('you need to define "olm-channel" flag to use "catalog-source-yaml".') + } + } else { + // Not OLM installer + if (flags['starting-csv']) { + this.error('"starting-csv" flag should be used only with "olm" installer.') + } + if (flags['catalog-source-yaml']) { + this.error('"catalog-source-yaml" flag should be used only with "olm" installer.') + } + if (flags['olm-channel']) { + this.error('"olm-channel" flag should be used only with "olm" installer.') + } + if (flags['package-manifest-name']) { + this.error('"package-manifest-name" flag should be used only with "olm" installer.') + } + if (flags['catalog-source-name']) { + this.error('"catalog-source-name" flag should be used only with "olm" installer.') + } + if (flags['catalog-source-namespace']) { + this.error('"package-manifest-name" flag should be used only with "olm" installer.') + } + if (flags['cluster-monitoring'] && flags.platform !== 'openshift') { + this.error('"cluster-monitoring" flag should be used only with "olm" installer and "openshift" platform.') + } + } + + if (flags.version) { + // Check minimal allowed version to install + let minAllowedVersion: string + switch (flags.installer) { + case 'olm': + minAllowedVersion = MIN_OLM_INSTALLER_VERSION + break + case 'operator': + minAllowedVersion = MIN_CHE_OPERATOR_INSTALLER_VERSION + break + case 'helm': + minAllowedVersion = MIN_HELM_INSTALLER_VERSION + break + default: + // Should never happen + minAllowedVersion = 'latest' + } + + let isVersionAllowed = false + try { + isVersionAllowed = semver.gte(flags.version, minAllowedVersion) + } catch (error) { + // not to fail unexpectedly + cli.debug(`Failed to compare versions '${flags.version}' and '${minAllowedVersion}': ${error}`) + } + + if (!isVersionAllowed) { + throw new Error(`This chectl version can deploy version ${minAllowedVersion} and higher. If you need to deploy ${flags.version} or lower, download the corresponding legacy chectl version.`) + } + } + } + + async run() { + const { flags } = this.parse(Deploy) + flags.chenamespace = flags.chenamespace || DEFAULT_CHE_NAMESPACE + const ctx = await ChectlContext.initAndGet(flags, this) + + if (!flags.batch && ctx.isChectl) { + await askForChectlUpdateIfNeeded() + } + + await this.setPlaformDefaults(flags, ctx) + await this.config.runHook(DEFAULT_ANALYTIC_HOOK_NAME, { command: Deploy.id, flags }) + + const cheTasks = new CheTasks(flags) + const platformTasks = new PlatformTasks() + const installerTasks = new InstallerTasks() + const apiTasks = new ApiTasks() + const devWorkspaceTasks = new DevWorkspaceTasks(flags) + + // Platform Checks + const platformCheckTasks = new Listr(platformTasks.preflightCheckTasks(flags, this), ctx.listrOptions) + + // Checks if Eclipse Che is already deployed + const preInstallTasks = new Listr(undefined, ctx.listrOptions) + preInstallTasks.add(apiTasks.testApiTasks(flags)) + preInstallTasks.add({ + title: '๐Ÿ‘€ Looking for an already existing Eclipse Che instance', + task: () => new Listr(cheTasks.checkIfCheIsInstalledTasks(flags)), + }) + preInstallTasks.add(checkChectlAndCheVersionCompatibility(flags)) + preInstallTasks.add(downloadTemplates(flags)) + preInstallTasks.add({ + title: '๐Ÿงช DevWorkspace engine (experimental / technology preview) ๐Ÿšจ', + enabled: () => (this.isDevWorkspaceEnabled(ctx) || flags['workspace-engine'] === 'dev-workspace') && !ctx.isOpenShift, + task: () => new Listr(devWorkspaceTasks.getInstallTasks(flags)), + }) + const installTasks = new Listr(installerTasks.installTasks(flags, this), ctx.listrOptions) + + // Post Install Checks + const postInstallTasks = new Listr([ + { + title: 'โœ… Post installation checklist', + task: () => new Listr(cheTasks.waitDeployedChe()), + }, + getRetrieveKeycloakCredentialsTask(flags), + retrieveCheCaCertificateTask(flags), + ...cheTasks.preparePostInstallationOutput(flags), + getPrintHighlightedMessagesTask(), + ], ctx.listrOptions) + + const logsTasks = new Listr([{ + title: 'Following Eclipse Che logs', + task: () => new Listr(cheTasks.serverLogsTasks(flags, true)), + }], ctx.listrOptions) + + try { + await preInstallTasks.run(ctx) + + if (ctx.isCheDeployed) { + let message = 'Eclipse Che has been already deployed.' + if (!ctx.isCheReady) { + message += ' Use server:start command to start a stopped Eclipse Che instance.' + } + cli.warn(message) + } else { + this.checkCompatibility(flags) + await platformCheckTasks.run(ctx) + await logsTasks.run(ctx) + await installTasks.run(ctx) + await postInstallTasks.run(ctx) + this.log(getCommandSuccessMessage()) + } + } catch (err) { + this.error(wrapCommandError(err)) + } + + notifyCommandCompletedSuccessfully() + this.exit(0) + } + + /** + * Sets default installer which is `olm` for OpenShift 4 with stable version of chectl + * and `operator` for other cases. + */ + async setDefaultInstaller(flags: any, _ctx: any): Promise { + const kubeHelper = new KubeHelper(flags) + + const isOlmPreinstalled = await kubeHelper.isPreInstalledOLM() + if ((flags['catalog-source-name'] || flags['catalog-source-yaml']) && isOlmPreinstalled) { + flags.installer = 'olm' + return + } + + if (flags.platform === 'openshift' && await kubeHelper.isOpenShift4() && isOlmPreinstalled) { + flags.installer = 'olm' + } else { + flags.installer = 'operator' + } + } } diff --git a/src/commands/server/logs.ts b/src/commands/server/logs.ts index c88296916..1af299979 100644 --- a/src/commands/server/logs.ts +++ b/src/commands/server/logs.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/server/start.ts b/src/commands/server/start.ts index c9c697d58..83ae7b095 100644 --- a/src/commands/server/start.ts +++ b/src/commands/server/start.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -21,65 +21,65 @@ import { ApiTasks } from '../../tasks/platforms/api' import { findWorkingNamespace, getCommandSuccessMessage, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' export default class Start extends Command { - static description = 'Start Eclipse Che server' + static description = 'Start Eclipse Che server' - static flags: flags.Input = { - help: flags.help({ char: 'h' }), - chenamespace: cheNamespace, - 'listr-renderer': listrRenderer, - 'deployment-name': cheDeployment, - [K8SPODWAITTIMEOUT_KEY]: k8sPodWaitTimeout, - [K8SPODREADYTIMEOUT_KEY]: k8sPodReadyTimeout, - [K8SPODDOWNLOADIMAGETIMEOUT_KEY]: k8sPodDownloadImageTimeout, - [K8SPODERRORRECHECKTIMEOUT_KEY]: k8sPodErrorRecheckTimeout, - [LOG_DIRECTORY_KEY]: logsDirectory, - 'skip-kubernetes-health-check': skipKubeHealthzCheck, - } + static flags: flags.Input = { + help: flags.help({ char: 'h' }), + chenamespace: cheNamespace, + 'listr-renderer': listrRenderer, + 'deployment-name': cheDeployment, + [K8SPODWAITTIMEOUT_KEY]: k8sPodWaitTimeout, + [K8SPODREADYTIMEOUT_KEY]: k8sPodReadyTimeout, + [K8SPODDOWNLOADIMAGETIMEOUT_KEY]: k8sPodDownloadImageTimeout, + [K8SPODERRORRECHECKTIMEOUT_KEY]: k8sPodErrorRecheckTimeout, + [LOG_DIRECTORY_KEY]: logsDirectory, + 'skip-kubernetes-health-check': skipKubeHealthzCheck, + } - async run() { - const { flags } = this.parse(Start) - flags.chenamespace = await findWorkingNamespace(flags) - const ctx = await ChectlContext.initAndGet(flags, this) + async run() { + const { flags } = this.parse(Start) + flags.chenamespace = await findWorkingNamespace(flags) + const ctx = await ChectlContext.initAndGet(flags, this) - const cheTasks = new CheTasks(flags) - const apiTasks = new ApiTasks() + const cheTasks = new CheTasks(flags) + const apiTasks = new ApiTasks() - // Checks if Eclipse Che is already deployed - const preInstallTasks = new Listr([ - apiTasks.testApiTasks(flags), - { - title: '๐Ÿ‘€ Looking for an already existing Eclipse Che instance', - task: () => new Listr(cheTasks.checkIfCheIsInstalledTasks(flags)), - }, - ], ctx.listrOptions) + // Checks if Eclipse Che is already deployed + const preInstallTasks = new Listr([ + apiTasks.testApiTasks(flags), + { + title: '๐Ÿ‘€ Looking for an already existing Eclipse Che instance', + task: () => new Listr(cheTasks.checkIfCheIsInstalledTasks(flags)), + }, + ], ctx.listrOptions) - const logsTasks = new Listr([{ - title: 'Following Eclipse Che logs', - task: () => new Listr(cheTasks.serverLogsTasks(flags, true)), - }], ctx.listrOptions) + const logsTasks = new Listr([{ + title: 'Following Eclipse Che logs', + task: () => new Listr(cheTasks.serverLogsTasks(flags, true)), + }], ctx.listrOptions) - const startCheTasks = new Listr([{ - title: 'Starting Eclipse Che', - task: () => new Listr(cheTasks.scaleCheUpTasks()), - }], ctx.listrOptions) + const startCheTasks = new Listr([{ + title: 'Starting Eclipse Che', + task: () => new Listr(cheTasks.scaleCheUpTasks()), + }], ctx.listrOptions) - try { - await preInstallTasks.run(ctx) + try { + await preInstallTasks.run(ctx) - if (!ctx.isCheDeployed) { - cli.warn('Eclipse Che has not been deployed yet. Use server:deploy command to deploy a new Eclipse Che instance.') - } else if (ctx.isCheReady) { - cli.info('Eclipse Che has been already started.') - } else { - await logsTasks.run(ctx) - await startCheTasks.run(ctx) - this.log(getCommandSuccessMessage()) - } - } catch (err) { - this.error(wrapCommandError(err)) - } + if (!ctx.isCheDeployed) { + cli.warn('Eclipse Che has not been deployed yet. Use server:deploy command to deploy a new Eclipse Che instance.') + } else if (ctx.isCheReady) { + cli.info('Eclipse Che has been already started.') + } else { + await logsTasks.run(ctx) + await startCheTasks.run(ctx) + this.log(getCommandSuccessMessage()) + } + } catch (err) { + this.error(wrapCommandError(err)) + } - notifyCommandCompletedSuccessfully() - this.exit(0) - } + notifyCommandCompletedSuccessfully() + this.exit(0) + } } diff --git a/src/commands/server/status.ts b/src/commands/server/status.ts index eb80e9594..152d7c234 100644 --- a/src/commands/server/status.ts +++ b/src/commands/server/status.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/server/stop.ts b/src/commands/server/stop.ts index f1d1e6f73..53f2d081a 100644 --- a/src/commands/server/stop.ts +++ b/src/commands/server/stop.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/server/update.ts b/src/commands/server/update.ts index 3e0568673..7a7a296f2 100644 --- a/src/commands/server/update.ts +++ b/src/commands/server/update.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -252,9 +252,10 @@ export default class Update extends Command { // Despite the operator image is the same, CR patch might contain some changes. cli.info('Patching existing Eclipse Che installation.') return true + } else { + cli.info('Eclipse Che is already up to date.') + return false } - cli.info('Eclipse Che is already up to date.') - return false } if (this.isUpgrade(ctx.deployedCheOperatorImageTag, ctx.newCheOperatorImageTag)) { diff --git a/src/commands/workspace/create.ts b/src/commands/workspace/create.ts index 05bb071a3..a9f3ffb32 100644 --- a/src/commands/workspace/create.ts +++ b/src/commands/workspace/create.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/workspace/delete.ts b/src/commands/workspace/delete.ts index f8c5c023c..f45b9c4f9 100644 --- a/src/commands/workspace/delete.ts +++ b/src/commands/workspace/delete.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/workspace/inject.ts b/src/commands/workspace/inject.ts index 4e5efaafc..c4d275b02 100644 --- a/src/commands/workspace/inject.ts +++ b/src/commands/workspace/inject.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -147,7 +147,9 @@ export default class Inject extends Command { const { exitCode } = await execa(`${this.command} exec ${pod} -n ${namespace} -c ${container} -- tar --version `, { timeout: 10000, reject: false, shell: true }) if (exitCode === 0) { return true - } return false + } else { + return false + } } /** @@ -211,13 +213,16 @@ export default class Inject extends Command { await execa(this.command, ['config', configPathFlag, kubeConfigPath, 'use-context', contextToInject.name], { timeout: 10000 }) await execa(this.command, ['cp', kubeConfigPath, `${namespace}/${workspacePod}:${containerHomeDir}.kube/config`, '-c', container], { timeout: 10000 }) + return } private async fileExists(namespace: string, pod: string, container: string, file: string): Promise { const { exitCode } = await execa(`${this.command} exec ${pod} -n ${namespace} -c ${container} -- test -e ${file}`, { timeout: 10000, reject: false, shell: true }) if (exitCode === 0) { return true - } return false + } else { + return false + } } private async containerExists(namespace: string, pod: string, container: string): Promise { diff --git a/src/commands/workspace/list.ts b/src/commands/workspace/list.ts index 996ac492b..a1a4ca146 100644 --- a/src/commands/workspace/list.ts +++ b/src/commands/workspace/list.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/workspace/logs.ts b/src/commands/workspace/logs.ts index 6b5be2895..b10e01456 100644 --- a/src/commands/workspace/logs.ts +++ b/src/commands/workspace/logs.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/workspace/start.ts b/src/commands/workspace/start.ts index 3550f7eb7..0cb521528 100644 --- a/src/commands/workspace/start.ts +++ b/src/commands/workspace/start.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/commands/workspace/stop.ts b/src/commands/workspace/stop.ts index 5fdda649e..31dfe0741 100644 --- a/src/commands/workspace/stop.ts +++ b/src/commands/workspace/stop.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/common-flags.ts b/src/common-flags.ts index 47ebd8474..f169ff93c 100644 --- a/src/common-flags.ts +++ b/src/common-flags.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/constants.ts b/src/constants.ts index 1f69cad4e..0d352a7e8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/hooks/analytics/analytics.ts b/src/hooks/analytics/analytics.ts index 362384090..867f2317d 100644 --- a/src/hooks/analytics/analytics.ts +++ b/src/hooks/analytics/analytics.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/hooks/analytics/segment-adapter.ts b/src/hooks/analytics/segment-adapter.ts index 65d3656e0..20a48fdb5 100644 --- a/src/hooks/analytics/segment-adapter.ts +++ b/src/hooks/analytics/segment-adapter.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/hooks/prerun/new-version-warning.ts b/src/hooks/prerun/new-version-warning.ts index 299e4351f..774bf8754 100644 --- a/src/hooks/prerun/new-version-warning.ts +++ b/src/hooks/prerun/new-version-warning.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/index.ts b/src/index.ts index 71556b35c..7aa48bea8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/che.ts b/src/tasks/che.ts index 52e5a664d..7e4743ccb 100644 --- a/src/tasks/che.ts +++ b/src/tasks/che.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/component-installers/cert-manager.ts b/src/tasks/component-installers/cert-manager.ts index 432e529c4..859adcf0c 100644 --- a/src/tasks/component-installers/cert-manager.ts +++ b/src/tasks/component-installers/cert-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/component-installers/devfile-workspace-operator-installer.ts b/src/tasks/component-installers/devfile-workspace-operator-installer.ts index 933720ac9..ccb00c9c3 100644 --- a/src/tasks/component-installers/devfile-workspace-operator-installer.ts +++ b/src/tasks/component-installers/devfile-workspace-operator-installer.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/installers/common-tasks.ts b/src/tasks/installers/common-tasks.ts index 6a63c9625..d8d404eca 100644 --- a/src/tasks/installers/common-tasks.ts +++ b/src/tasks/installers/common-tasks.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/installers/helm.ts b/src/tasks/installers/helm.ts index c888c9354..f2e3e9f2d 100644 --- a/src/tasks/installers/helm.ts +++ b/src/tasks/installers/helm.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -238,21 +238,27 @@ export class HelmTasks { const { exitCode } = await execa('kubectl', ['get', 'clusterrolebinding', `${cheNamespace}-che-clusterrole-binding`], { timeout: execTimeout, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async removeClusterRoleBinding(cheNamespace: string, execTimeout = 30000): Promise { const { exitCode } = await execa('kubectl', ['delete', 'clusterrolebinding', `${cheNamespace}-che-clusterrole-binding`], { timeout: execTimeout, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async tillerRoleBindingExist(execTimeout = 30000): Promise { const { exitCode } = await execa('kubectl', ['get', 'clusterrolebinding', 'add-on-cluster-admin'], { timeout: execTimeout, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async createTillerRoleBinding(execTimeout = 30000) { @@ -263,7 +269,9 @@ export class HelmTasks { const { exitCode } = await execa('kubectl', ['get', 'serviceaccounts', 'tiller', '--namespace', 'kube-system'], { timeout: execTimeout, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async createTillerServiceAccount(execTimeout = 120000) { @@ -281,7 +289,9 @@ export class HelmTasks { const { exitCode } = await execa('kubectl', ['get', 'services', 'tiller-deploy', '-n', 'kube-system'], { timeout: execTimeout, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async getVersion(execTimeout = 10000): Promise { diff --git a/src/tasks/installers/installer.ts b/src/tasks/installers/installer.ts index 298519c9f..97100d22e 100644 --- a/src/tasks/installers/installer.ts +++ b/src/tasks/installers/installer.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/installers/olm.ts b/src/tasks/installers/olm.ts index d89f4980b..95c9f7bc6 100644 --- a/src/tasks/installers/olm.ts +++ b/src/tasks/installers/olm.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -430,8 +430,9 @@ export class OLMTasks { if (csv && csv.metadata.annotations) { const CRRaw = csv.metadata.annotations!['alm-examples'] return (yaml.safeLoad(CRRaw) as Array)[0] + } else { + throw new Error(`Unable to retrieve Che cluster CR definition from CSV: ${currentCSV}`) } - throw new Error(`Unable to retrieve Che cluster CR definition from CSV: ${currentCSV}`) } private getOlmNamespaceLabels(flags: any): any { diff --git a/src/tasks/installers/operator.ts b/src/tasks/installers/operator.ts index 5d583fb27..f742ca4db 100644 --- a/src/tasks/installers/operator.ts +++ b/src/tasks/installers/operator.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/kube.ts b/src/tasks/kube.ts index 50d63380c..c899d3efc 100644 --- a/src/tasks/kube.ts +++ b/src/tasks/kube.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/platforms/api.ts b/src/tasks/platforms/api.ts index 181898f08..2cb0d8045 100644 --- a/src/tasks/platforms/api.ts +++ b/src/tasks/platforms/api.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -17,10 +17,10 @@ import { newError } from '../../util' export class ApiTasks { /** - * Returns tasks which tests if K8s or OpenShift API is configured in the current context. - * - * `isOpenShift` property is provisioned into context. - */ + * Returns tasks which tests if K8s or OpenShift API is configured in the current context. + * + * `isOpenShift` property is provisioned into context. + */ testApiTasks(flags: any): Listr.ListrTask { const kube = new KubeHelper(flags) return { diff --git a/src/tasks/platforms/common-platform-tasks.ts b/src/tasks/platforms/common-platform-tasks.ts index d6a122e83..a71f21987 100644 --- a/src/tasks/platforms/common-platform-tasks.ts +++ b/src/tasks/platforms/common-platform-tasks.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/platforms/crc.ts b/src/tasks/platforms/crc.ts index 57076dd70..612220ae6 100644 --- a/src/tasks/platforms/crc.ts +++ b/src/tasks/platforms/crc.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -74,8 +74,9 @@ export class CRCHelper { stdout.includes('CRC VM: Running') && stdout.includes('OpenShift: Running')) { return true + } else { + return false } - return false } async getCRCIP(): Promise { diff --git a/src/tasks/platforms/docker-desktop.ts b/src/tasks/platforms/docker-desktop.ts index 4163bd14e..17d4af15a 100644 --- a/src/tasks/platforms/docker-desktop.ts +++ b/src/tasks/platforms/docker-desktop.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/platforms/k8s.ts b/src/tasks/platforms/k8s.ts index dd731567f..c1a6c051c 100644 --- a/src/tasks/platforms/k8s.ts +++ b/src/tasks/platforms/k8s.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/platforms/microk8s.ts b/src/tasks/platforms/microk8s.ts index 2ce059de3..afbac03ab 100644 --- a/src/tasks/platforms/microk8s.ts +++ b/src/tasks/platforms/microk8s.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -106,7 +106,9 @@ export class MicroK8sTasks { const { exitCode } = await execa('microk8s.status', { timeout: 10000, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async startMicroK8s() { diff --git a/src/tasks/platforms/minikube.ts b/src/tasks/platforms/minikube.ts index 7f62984bb..d357f5440 100644 --- a/src/tasks/platforms/minikube.ts +++ b/src/tasks/platforms/minikube.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -135,7 +135,9 @@ export class MinikubeTasks { const { exitCode } = await execa('minikube', ['status'], { timeout: 10000, reject: false }) if (exitCode === 0) { return true - } return false + } else { + return false + } } async startMinikube() { @@ -149,7 +151,6 @@ export class MinikubeTasks { // grab json const json = JSON.parse(stdout) return json.ingress && json.ingress.Status === 'enabled' - // eslint-disable-next-line no-else-return } else { // probably with old minikube, let's try with classic output const { stdout } = await execa('minikube', ['addons', 'list'], { timeout: 10000 }) diff --git a/src/tasks/platforms/minishift.ts b/src/tasks/platforms/minishift.ts index 71b171493..643703473 100644 --- a/src/tasks/platforms/minishift.ts +++ b/src/tasks/platforms/minishift.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -65,8 +65,9 @@ export class MinishiftTasks { stdout.includes('Minishift: Running') && stdout.includes('OpenShift: Running')) { return true + } else { + return false } - return false } async getMinishiftIP(): Promise { diff --git a/src/tasks/platforms/openshift.ts b/src/tasks/platforms/openshift.ts index 0b2231585..0bf98fcdc 100644 --- a/src/tasks/platforms/openshift.ts +++ b/src/tasks/platforms/openshift.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/tasks/platforms/platform.ts b/src/tasks/platforms/platform.ts index 6819f4c8c..e0419b002 100644 --- a/src/tasks/platforms/platform.ts +++ b/src/tasks/platforms/platform.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ diff --git a/src/util.ts b/src/util.ts index 34f462bea..8e0f37e4c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2019-2021 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/ @@ -333,7 +333,8 @@ export async function getDistribution(): Promise { const platorm = await promisify(getos)() as getos.LinuxOs return platorm.dist } catch { - + return } } + return }