From 57d1e1f9e3923b238a9f8f9dbe67b8ead2d72e8f Mon Sep 17 00:00:00 2001 From: Mykola Morhun Date: Tue, 9 Feb 2021 17:49:17 +0200 Subject: [PATCH] Improve Eclipse Che deploy / update flow (#1082) Improve Eclipse Che deploying with chectl Signed-off-by: Mykola Morhun --- README.md | 25 +- package.json | 33 +- src/api/che.ts | 69 ++- src/api/context.ts | 12 +- src/api/github-client.ts | 239 ++++++++ src/api/kube.ts | 213 +++++--- src/api/version.ts | 114 +++- src/commands/server/delete.ts | 10 +- src/commands/server/deploy.ts | 152 +++--- src/commands/server/update.ts | 300 +++++++---- src/common-flags.ts | 14 + src/constants.ts | 8 +- src/hooks/prerun/new-version-warning.ts | 37 ++ .../component-installers/cert-manager.ts | 8 +- src/tasks/installers/common-tasks.ts | 76 ++- src/tasks/installers/helm.ts | 74 ++- src/tasks/installers/installer.ts | 18 +- src/tasks/installers/minishift-addon.ts | 200 ------- src/tasks/installers/olm.ts | 62 ++- src/tasks/installers/operator.ts | 510 +++++++----------- src/util.ts | 98 ++-- test/api/version.test.ts | 54 +- test/tasks/installers/minishift-addon.test.ts | 63 --- yarn.lock | 268 ++++++++- 24 files changed, 1682 insertions(+), 975 deletions(-) create mode 100644 src/api/github-client.ts create mode 100644 src/hooks/prerun/new-version-warning.ts delete mode 100644 src/tasks/installers/minishift-addon.ts delete mode 100644 test/tasks/installers/minishift-addon.test.ts diff --git a/README.md b/README.md index 810de184c..c461e05c0 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,9 @@ OPTIONS answer to all prompts and run non-interactively + --batch Batch mode. Running a command without end + user interaction. + --delete-namespace Indicates that a Eclipse Che namespace will be deleted as well @@ -442,7 +445,7 @@ USAGE $ chectl server:deploy OPTIONS - -a, --installer=helm|operator|olm|minishift-addon + -a, --installer=helm|operator|olm Installer type. If not set, default is "olm" for OpenShift 4.x platform otherwise "operator". -b, --domain=domain @@ -479,12 +482,18 @@ OPTIONS -t, --templates=templates Path to the templates folder + -v, --version=version + Version to deploy (e.g. 7.15.2). Defaults to the same as chectl. + --[no-]auto-update 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'. + --batch + Batch mode. Running a command without end user interaction. + --catalog-source-name=catalog-source-name OLM catalog source to install Eclipse Che operator. This parameter is used only when the installer is the 'olm'. @@ -508,8 +517,7 @@ OPTIONS is the 'operator' or the 'olm'. --che-operator-image=che-operator-image - [default: quay.io/eclipse/che-operator:nightly] Container image of the operator. This parameter is used only when - the installer is the operator + Container image of the operator. This parameter is used only when the installer is the operator --debug Enables the debug mode for Eclipse Che server. To debug Eclipse Che server from localhost use 'server:debug' @@ -714,19 +722,20 @@ USAGE OPTIONS -h, --help show CLI help -n, --chenamespace=chenamespace Eclipse Che Kubernetes namespace. Default to 'eclipse-che' - -t, --templates=templates [default: templates] Path to the templates folder + -t, --templates=templates Path to the templates folder + + -v, --version=version Version to deploy (e.g. 7.15.2). Defaults to the same as + chectl. -y, --yes Automatic yes to prompts; assume "yes" as answer to all prompts and run non-interactively + --batch Batch mode. Running a command without end user interaction. + --che-operator-cr-patch-yaml=che-operator-cr-patch-yaml Path to a yaml file that overrides the default values in CheCluster CR used by the operator. This parameter is used only when the installer is the 'operator' or the 'olm'. - --che-operator-image=che-operator-image [default: quay.io/eclipse/che-operator:nightly] Container - image of the operator. This parameter is used only when the - installer is the operator - --deployment-name=deployment-name [default: che] Eclipse Che deployment name --skip-kubernetes-health-check Skip Kubernetes health check diff --git a/package.json b/package.json index 6d0cdc046..ec5b7d03c 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,7 @@ "@oclif/plugin-autocomplete": "^0.2.0", "@oclif/plugin-help": "^3", "@oclif/plugin-update": "^1.3.10", - "@types/command-exists": "^1.2.0", - "@types/fs-extra": "^9.0.1", - "@types/inquirer": "^7.3.1", - "@types/node-notifier": "^8.0.0", - "@types/request": "^2.48.5", - "@types/websocket": "^1.0.1", - "@types/ws": "^7.2.6", + "@octokit/rest": "^18.0.12", "analytics-node": "^3.4.0-beta.3", "ansi-colors": "4.1.1", "axios": "^0.19.2", @@ -47,8 +41,11 @@ "node-forge": "^0.10.0", "node-notifier": "^8.0.0", "querystring": "^0.2.0", + "rimraf": "^3.0.2", + "semver": "^7.3.4", "stream-buffers": "^3.0.2", - "tslib": "^1" + "tslib": "^1", + "unzipper": "0.10.11" }, "devDependencies": { "@eclipse-che/api": "latest", @@ -56,11 +53,21 @@ "@oclif/test": "^1", "@oclif/tslint": "^3", "@types/chai": "^4", + "@types/command-exists": "^1.2.0", + "@types/fs-extra": "^9.0.1", + "@types/inquirer": "^7.3.1", "@types/jest": "26.0.14", "@types/js-yaml": "^3.12.5", "@types/listr": "^0.14.2", "@types/node": "^12", "@types/node-forge": "^0.9.5", + "@types/node-notifier": "^8.0.0", + "@types/request": "^2.48.5", + "@types/rimraf": "^3.0.0", + "@types/semver": "^7.3.4", + "@types/unzipper": "^0.10.3", + "@types/websocket": "^1.0.1", + "@types/ws": "^7.2.6", "chai": "^4.2.0", "cpx": "^1.5.0", "globby": "^11", @@ -90,13 +97,14 @@ "main": "lib/index.js", "oclif": { "commands": "./lib/commands", + "hooks": { + "prerun": "./lib/hooks/prerun/new-version-warning", + "analytics": "./lib/hooks/analytics/analytics" + }, "bin": "chectl", "macos": { "identifier": "che-incubator.chectl" }, - "hooks": { - "analytics": "./lib/hooks/analytics/analytics" - }, "plugins": [ "@oclif/plugin-autocomplete", "@oclif/plugin-help", @@ -133,12 +141,11 @@ }, "repository": "che-incubator/chectl", "scripts": { - "postinstall": "npm run -s postinstall-repositories && npm run -s postinstall-helm && npm run -s postinstall-cert-manager && npm run -s postinstall-operator && npm run -s postinstall-minishift-addon && npm run -s postinstall-devfile-api && npm run -s postinstall-dev-workspace && npm run -s postinstall-cleanup", + "postinstall": "npm run -s postinstall-repositories && npm run -s postinstall-helm && npm run -s postinstall-cert-manager && npm run -s postinstall-operator && npm run -s postinstall-devfile-api && npm run -s postinstall-dev-workspace && npm run -s postinstall-cleanup", "postinstall-helm": "rimraf templates/kubernetes && cpx 'node_modules/eclipse-che/deploy/kubernetes/**' 'templates/kubernetes'", "postinstall-cert-manager": "rimraf templates/cert-manager && cpx 'node_modules/eclipse-che/deploy/cert-manager/**' 'templates/cert-manager'", "postinstall-devfile-api": "rimraf templates/devfile-api && cpx 'node_modules/eclipse-che-devfile-api/deploy/**' 'templates/devfile-api'", "postinstall-dev-workspace": "rimraf templates/devworkspace && cpx 'node_modules/eclipse-che-devfile-workspace-operator/deploy/**' 'templates/devworkspace'", - "postinstall-minishift-addon": "rimraf templates/minishift-addon && cpx 'node_modules/eclipse-che-minishift/addons/che/**' 'templates/minishift-addon/che'", "postinstall-operator": "rimraf templates/che-operator && cpx 'node_modules/eclipse-che-operator/deploy/**' 'templates/che-operator'", "postinstall-repositories": "yarn upgrade eclipse-che eclipse-che-operator eclipse-che-minishift eclipse-che-devfile-api eclipse-che-devfile-workspace-operator", "postinstall-cleanup": "rimraf node_modules/eclipse-che && rimraf node_modules/eclipse-che-operator && rimraf node_modules/eclipse-che-minishift", diff --git a/src/api/che.ts b/src/api/che.ts index e77d135dc..05b58e0da 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -20,10 +20,12 @@ import * as yaml from 'js-yaml' import * as nodeforge from 'node-forge' import * as os from 'os' import * as path from 'path' +import * as rimraf from 'rimraf' +import * as unzipper from 'unzipper' import { OpenShiftHelper } from '../api/openshift' import { CHE_ROOT_CA_SECRET_NAME, DEFAULT_CA_CERT_FILE_NAME } from '../constants' -import { base64Decode } from '../util' +import { base64Decode, downloadFile } from '../util' import { CheApiClient } from './che-api-client' import { ChectlContext } from './context' @@ -484,4 +486,69 @@ export class CheHelper { return fileName } + /** + * Gets install templates for given installer. + * @param installer Che installer + * @param url link to zip archive with sources of Che operator + * @param destDir destination directory into which the templates should be unpacked + */ + async downloadAndUnpackTemplates(installer: string, url: string, destDir: string): Promise { + // Add che-operator folder for operator templates + if (installer === 'operator') { + destDir = path.join(destDir, 'che-operator') + } + // No need to add kubernetes folder for Helm installer as it already present in the archive + + const tempDir = path.join(os.tmpdir(), Date.now().toString()) + await fs.mkdirp(tempDir) + const zipFile = path.join(tempDir, `che-templates-${installer}.zip`) + await downloadFile(url, zipFile) + await this.unzipTemplates(zipFile, destDir) + // Clean up zip. Do not wait when finishes. + rimraf(tempDir, () => {}) + } + + /** + * Unpacks repository deploy templates into specified folder + * @param zipFile path to zip archive with source code + * @param destDir target directory into which templates should be unpacked + */ + private async unzipTemplates(zipFile: string, destDir: string) { + // Gets path from: repo-name/deploy/path + const deployDirRegex = new RegExp('(?:^[\\\w-]*\\\/deploy\\\/)(.*)') + + const zip = fs.createReadStream(zipFile).pipe(unzipper.Parse({ forceStream: true })) + for await (const entry of zip) { + const entryPathInZip: string = entry.path + const templatesPathMatch = entryPathInZip.match(deployDirRegex) + if (templatesPathMatch && templatesPathMatch.length > 1 && templatesPathMatch[1]) { + // Remove prefix from in-zip path + const entryPathWhenExtracted = templatesPathMatch[1] + // Path to the item in target location + const dest = path.join(destDir, entryPathWhenExtracted) + + // Extract item + if (entry.type === 'File') { + const parentDirName = path.dirname(dest) + if (!fs.existsSync(parentDirName)) { + await fs.mkdirp(parentDirName) + } + entry.pipe(fs.createWriteStream(dest)) + } else if (entry.type === 'Directory') { + if (!fs.existsSync(dest)) { + await fs.mkdirp(dest) + } + // The folder is created above + entry.autodrain() + } else { + // Ignore the item as we do not need to handle links and etc. + entry.autodrain() + } + } else { + // No need to extract this item + entry.autodrain() + } + } + } + } diff --git a/src/api/context.ts b/src/api/context.ts index 6ba1c0478..18d3b0b56 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -14,9 +14,10 @@ import * as os from 'os' import * as path from 'path' import { CHE_OPERATOR_CR_PATCH_YAML_KEY, CHE_OPERATOR_CR_YAML_KEY, LOG_DIRECTORY_KEY } from '../common-flags' -import { readCRFile } from '../util' +import { getProjectName, getProjectVersion, readCRFile } from '../util' import { KubeHelper } from './kube' +import { CHECTL_DEVELOPMENT_VERSION } from './version' /** * chectl command context. @@ -28,13 +29,14 @@ export namespace ChectlContext { export const START_TIME = 'startTime' export const END_TIME = 'endTime' export const CONFIG_DIR = 'configDir' + export const CACHE_DIR = 'cacheDir' export const ERROR_LOG = 'errorLog' export const COMMAND_ID = 'commandId' // command specific attributes export const CUSTOM_CR = 'customCR' export const CR_PATCH = 'crPatch' - export const LOGS_DIRECTORY = 'directory' + export const LOGS_DIR = 'directory' const ctx: any = {} @@ -43,6 +45,9 @@ export namespace ChectlContext { ctx[IS_OPENSHIFT] = await kube.isOpenShift() ctx[IS_OPENSHIFT4] = await kube.isOpenShift4() + ctx.isChectl = getProjectName() === 'chectl' + ctx.isNightly = getProjectVersion().includes('next') || getProjectVersion() === CHECTL_DEVELOPMENT_VERSION + if (flags['listr-renderer'] as any) { ctx.listrOptions = { renderer: (flags['listr-renderer'] as any), collapse: false } as Listr.ListrOptions } @@ -51,9 +56,10 @@ export namespace ChectlContext { ctx[START_TIME] = Date.now() ctx[CONFIG_DIR] = command.config.configDir + ctx[CACHE_DIR] = command.config.cacheDir ctx[ERROR_LOG] = command.config.errlog ctx[COMMAND_ID] = command.id - ctx[LOGS_DIRECTORY] = path.resolve(flags[LOG_DIRECTORY_KEY] ? flags[LOG_DIRECTORY_KEY] : path.resolve(os.tmpdir(), 'chectl-logs', Date.now().toString())) + ctx[LOGS_DIR] = path.resolve(flags[LOG_DIRECTORY_KEY] ? flags[LOG_DIRECTORY_KEY] : path.resolve(os.tmpdir(), 'chectl-logs', Date.now().toString())) ctx[CUSTOM_CR] = readCRFile(flags, CHE_OPERATOR_CR_YAML_KEY) ctx[CR_PATCH] = readCRFile(flags, CHE_OPERATOR_CR_PATCH_YAML_KEY) diff --git a/src/api/github-client.ts b/src/api/github-client.ts new file mode 100644 index 000000000..6f81a7f32 --- /dev/null +++ b/src/api/github-client.ts @@ -0,0 +1,239 @@ +/********************************************************************* + * Copyright (c) 2020-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/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { Octokit } from '@octokit/rest' + +const OWNER = 'eclipse' +export const CHE_REPO = 'che' +export const CHE_OPERATOR_REPO = 'che-operator' + +export interface TagInfo { + name: string + commit: { + sha: string + url: string + } + zipball_url: string +} + +export class CheGithubClient { + private readonly octokit: Octokit + + constructor() { + this.octokit = new Octokit({ + baseUrl: 'https://api.github.com', + userAgent: 'chectl', + auth: process.env.GITHUB_TOKEN, + }) + } + + /** + * Returns version (tag) information based on installer and version string (e.g. 7.19.2). + */ + async getTemplatesTagInfo(installer: string, version?: string): Promise { + if (installer === 'operator' || installer === 'olm') { + return this.getTagInfoByVersion(CHE_OPERATOR_REPO, version) + } else if (installer === 'helm') { + return this.getTagInfoByVersion(CHE_REPO, version) + } + throw new Error(`Unsupported installer: ${installer}`) + } + + /** + * Gets last 50 tags from the given repository. + * @param repo repository name to list tag in + * @param prefix return only tags that starts with given prefix + */ + private async listLatestTags(repo: string, prefix = ''): Promise { + let response = await this.octokit.repos.listTags({ owner: OWNER, repo, per_page: 50 }) + const tags = response.data + if (prefix) { + return tags.filter(tag => tag.name.startsWith(prefix)) + } + return tags + } + + /** + * Gets tag info if it exists. + * @param repo repository name to search for the tag in + * @param tagName name of the tag + */ + private async getTag(repo: string, tagName: string): Promise { + try { + const tagRefResp = await this.octokit.git.getRef({ owner: OWNER, repo, ref: `tags/${tagName}` }) + const tagRef = tagRefResp.data + const downloadUrlResp = await this.octokit.repos.downloadZipballArchive({ owner: OWNER, repo, ref: tagRef.object.sha }) + // Simulate tag info + return { + name: tagName, + commit: { + sha: tagRef.object.sha, + url: tagRef.object.url, + }, + zipball_url: downloadUrlResp.url, + } + } catch (e) { + if (e.status !== 404) { + throw e + } + // Not found, return undefined + } + } + + /** + * Returns latest commit information in tag format. + * @param repo repository name to get the latest commit from + */ + private async getLastCommitInfo(repo: string): Promise { + const listCommitsResponse = await this.octokit.repos.listCommits({ owner: OWNER, repo, per_page: 1 }) + if (listCommitsResponse.status !== 200) { + throw new Error(`Failed to get list of commits from the repository '${repo}'. Request: ${listCommitsResponse.url}, response: ${listCommitsResponse.status}`) + } + const lastCommit = listCommitsResponse.data[0] + + const downloadZipResponse = await this.octokit.repos.downloadZipballArchive({ + owner: OWNER, + repo, + ref: lastCommit.sha!, + }) + const zipball_url = downloadZipResponse.url + + // Simiulate tag info to have similar return type + return { + name: 'next', + commit: { + sha: lastCommit.sha!, + url: lastCommit.commit.url, + }, + zipball_url, + } + } + + /** + * Returns tag/commit information about given version. + * The informaton includes zip archive download link. + * If non-existing version is given, then undefined will be returned. + * @param repo repository name + * @param version version or version prefix. If only prefix is given, the latest one that match will be choosen. + */ + private async getTagInfoByVersion(repo: string, version?: string): Promise { + if (!version || version === 'latest' || version === 'stable') { + const tags = await this.listLatestTags(repo) + return this.getLatestTag(tags) + } 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 + } + } + + /** + * Helper for getTagInfoByVersion + * Gets tag by exact match or latest tag with given prefix + * @param repo repository name + * @param versionPrefix version or version prefix, e.g. 7.22.0 or 7.18 + */ + private async getTagInfoByVersionPrefix(repo: string, versionPrefix: string): Promise { + let tagInfo = await this.getTag(repo, versionPrefix) + if (tagInfo) { + // Exact match found + return tagInfo + } + + 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 { + // Several tags match the given version (e.g. 7.15.0 and 7.15.1 match 7.15). + // Find the latest one. + return this.getLatestTag(tags) + } + } + + /** + * Finds the latest tag of format x.y.z, where x,y and z are numbers. + * @param tags repository tags list returned by octokit + */ + private getLatestTag(tags: TagInfo[]): TagInfo { + if (tags.length === 0) { + throw new Error('Tag list should not be empty') + } + + const sortedSemanticTags = this.sortSemanticTags(tags) + return sortedSemanticTags[0] + } + + /** + * Sorts given tags. First is the latest. + * All tags should use semantic versioning in form x.y.z, where x,y and z are numbers. + * If a tag is not in the descrbed above format, it will be ignored. + * @param tags list of tags to sort + */ + private sortSemanticTags(tags: TagInfo[]): TagInfo[] { + interface SemanticTagData { + major: number + minor: number + patch: number + data: TagInfo + } + + const semanticTags: SemanticTagData[] = tags.reduce((acceptedTags, tagInfo, _index: number, _all: TagInfo[]) => { + // Remove 'v' prefix if any + if (tagInfo.name.startsWith('v')) { + tagInfo.name = tagInfo.name.substring(1) + } + + const versionComponents = tagInfo.name.split('.') + // Accept the tag only if it has format x.y.z and z has no suffix (like '-RC2' or '-5e87ab1') + if (versionComponents.length === 3 && (parseInt(versionComponents[2], 10).toString() === versionComponents[2])) { + acceptedTags.push({ + major: parseInt(versionComponents[0], 10), + minor: parseInt(versionComponents[1], 10), + patch: parseInt(versionComponents[2], 10), + data: tagInfo, + }) + } + return acceptedTags + }, []) + if (semanticTags.length === 0) { + // Should never happen + throw new Error('There is no semantic tags') + } + + const sortedSemanticTags = semanticTags.sort((semTagA: SemanticTagData, semTagB: SemanticTagData) => { + if (semTagA.major !== semTagB.major) { + return semTagB.major - semTagA.major + } else if (semTagA.minor !== semTagB.minor) { + return semTagB.minor - semTagA.minor + } else if (semTagA.patch !== semTagB.patch) { + return semTagB.patch - semTagA.patch + } else { + return 0 + } + }) + + return sortedSemanticTags.map(tag => tag.data) + } + +} diff --git a/src/api/kube.ts b/src/api/kube.ts index 3c48145f5..475db53b8 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -8,24 +8,24 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, AuthorizationV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, ExtensionsV1beta1IngressList, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1ContainerStateTerminated, V1ContainerStateWaiting, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1Namespace, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodCondition, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1SelfSubjectAccessReview, V1SelfSubjectAccessReviewSpec, V1Service, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' +import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, AuthorizationV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, ExtensionsV1beta1IngressList, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1ClusterRole, V1ClusterRoleBinding, V1ClusterRoleBindingList, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1ContainerStateTerminated, V1ContainerStateWaiting, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1Namespace, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodCondition, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleBindingList, V1RoleList, V1RoleRef, V1Secret, V1SelfSubjectAccessReview, V1SelfSubjectAccessReviewSpec, V1Service, V1ServiceAccount, V1ServiceList, V1Status, V1Subject, Watch } from '@kubernetes/client-node' import { Cluster, Context } from '@kubernetes/client-node/dist/config_types' import axios, { AxiosRequestConfig } from 'axios' import { cli } from 'cli-ux' import * as execa from 'execa' import * as fs from 'fs' import https = require('https') -import * as yaml from 'js-yaml' import { merge } from 'lodash' import * as net from 'net' import { Writable } from 'stream' -import { CHE_CLUSTER_CRD, DEFAULT_CHE_IMAGE, DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT, DEFAULT_K8S_POD_WAIT_TIMEOUT, OLM_STABLE_CHANNEL_NAME } from '../constants' -import { getClusterClientCommand, isKubernetesPlatformFamily } from '../util' +import { CHE_CLUSTER_CRD, DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT, DEFAULT_K8S_POD_WAIT_TIMEOUT, OLM_STABLE_CHANNEL_NAME } from '../constants' +import { getClusterClientCommand, isKubernetesPlatformFamily, safeLoadFromYamlFile } from '../util' import { V1alpha2Certificate } from './typings/cert-manager' import { CatalogSource, ClusterServiceVersion, ClusterServiceVersionList, InstallPlan, OperatorGroup, PackageManifest, Subscription } from './typings/olm' import { IdentityProvider, OAuth } from './typings/openshift' +import { VersionHelper } from './version' const AWAIT_TIMEOUT_S = 30 @@ -246,8 +246,30 @@ export class KubeHelper { } } - async createRoleFromFile(filePath: string, namespace = '') { - const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role + async getRole(name: string, namespace: string): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.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 = KubeHelper.KUBE_CONFIG.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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { const res = await k8sRbacAuthApi.createNamespacedRole(namespace, yamlRole) @@ -257,12 +279,16 @@ export class KubeHelper { } } - async replaceRoleFromFile(filePath: string, namespace = '') { + 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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) if (!yamlRole.metadata || !yamlRole.metadata.name) { - throw new Error(`Role read from ${filePath} must have name specified`) + throw new Error('Role object requires name') } try { const res = await k8sRbacAuthApi.replaceNamespacedRole(yamlRole.metadata.name, namespace, yamlRole) @@ -272,46 +298,69 @@ export class KubeHelper { } } - async createClusterRoleFromFile(filePath: string, roleName?: string) { - const yamlRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole + async replaceRoleFromFile(filePath: string, namespace: string) { + const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role + return this.replaceRoleFrom(yamlRole, namespace) + } + + async listClusterRoles(): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) - if (!yamlRole.metadata) { - yamlRole.metadata = {} + if (!yamlClusterRole.metadata) { + yamlClusterRole.metadata = {} } - if (roleName) { - yamlRole.metadata.name = roleName - } else if (!yamlRole.metadata.name) { - throw new Error(`Role name is not specified in ${filePath}`) + 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(yamlRole) + const res = await k8sRbacAuthApi.createClusterRole(yamlClusterRole) return res.response.statusCode } catch (e) { throw this.wrapK8sClientError(e) } } - async replaceClusterRoleFromFile(filePath: string, roleName?: string) { - const yamlRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole + 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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) - if (!yamlRole.metadata) { - yamlRole.metadata = {} + if (!yamlClusterRole.metadata) { + yamlClusterRole.metadata = {} } - if (roleName) { - yamlRole.metadata.name = roleName - } else if (!yamlRole.metadata.name) { - throw new Error(`Role name is not specified in ${filePath}`) + 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(yamlRole.metadata.name, yamlRole) + 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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) const clusterRole = await this.getClusterRole(name) @@ -367,6 +416,16 @@ export class KubeHelper { } } + async listRoleBindings(namespace: string): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { @@ -377,7 +436,17 @@ export class KubeHelper { } } - async clusterRoleBindingExist(name = ''): Promise { + async listClusterRoleBindings(labelSelector?: string, fieldSelector?: string): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { const { body } = await k8sRbacAuthApi.readClusterRoleBinding(name) @@ -408,31 +477,45 @@ export class KubeHelper { } } - async createRoleBindingFromFile(filePath: string, namespace = '') { - const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding + async createRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { - return await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, yamlRoleBinding) + const response = await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, yamlRoleBinding) + return response.body } catch (e) { throw this.wrapK8sClientError(e) } } - async replaceRoleBindingFromFile(filePath: string, namespace = '') { + 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(`Role binding read from ${filePath} must have name specified`) + throw new Error('RoleBinding object requires name') } const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { - return await k8sRbacAuthApi.replaceNamespacedRoleBinding(yamlRoleBinding.metadata.name, namespace, yamlRoleBinding) + 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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { return await k8sRbacAuthApi.createClusterRoleBinding(yamlClusterRoleBinding) @@ -461,9 +544,17 @@ export class KubeHelper { 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 = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { - return await k8sRbacAuthApi.createClusterRoleBinding(clusterRoleBinding) + return await k8sRbacAuthApi.replaceClusterRoleBinding(clusterRoleBinding.metadata.name, clusterRoleBinding) } catch (e) { throw this.wrapK8sClientError(e) } @@ -488,12 +579,7 @@ export class KubeHelper { apiGroup: 'rbac.authorization.k8s.io' } } as V1ClusterRoleBinding - const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) - try { - return await k8sRbacAuthApi.replaceClusterRoleBinding(name, clusterRoleBinding) - } catch (e) { - throw this.wrapK8sClientError(e) - } + return this.replaceClusterRoleBindingFrom(clusterRoleBinding) } async deleteRoleBinding(name = '', namespace = '') { @@ -505,10 +591,11 @@ export class KubeHelper { } } - async deleteClusterRoleBinding(name = '') { + async deleteClusterRoleBinding(name: string): Promise { const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { - return await k8sRbacAuthApi.deleteClusterRoleBinding(name) + const res = await k8sRbacAuthApi.deleteClusterRoleBinding(name) + return res.body } catch (e) { throw this.wrapK8sClientError(e) } @@ -975,15 +1062,18 @@ export class KubeHelper { } } - async createDeploymentFromFile(filePath: string, namespace = '', containerImage = '', containerIndex = 0) { + async createDeploymentFromFile(filePath: string, namespace?: string, containerImage?: string, containerIndex = 0) { const yamlDeployment = this.safeLoadFromYamlFile(filePath) as V1Deployment if (containerImage) { yamlDeployment.spec!.template.spec!.containers[containerIndex].image = containerImage } + if (!namespace) { + namespace = yamlDeployment.metadata && yamlDeployment.metadata.namespace || '' + } return this.createDeploymentFrom(yamlDeployment, namespace) } - async createDeploymentFrom(yamlDeployment: V1Deployment, namespace = '') { + async createDeploymentFrom(yamlDeployment: V1Deployment, namespace: string) { const k8sAppsApi = KubeHelper.KUBE_CONFIG.makeApiClient(AppsV1Api) try { return await k8sAppsApi.createNamespacedDeployment(namespace, yamlDeployment) @@ -1001,11 +1091,14 @@ export class KubeHelper { } } - async replaceDeploymentFromFile(filePath: string, namespace = '', containerImage = '', containerIndex = 0) { + async replaceDeploymentFromFile(filePath: string, namespace?: string, containerImage?: string, containerIndex = 0) { const yamlDeployment = this.safeLoadFromYamlFile(filePath) as V1Deployment if (containerImage) { yamlDeployment.spec!.template.spec!.containers[containerIndex].image = containerImage } + if (!namespace) { + namespace = yamlDeployment.metadata && yamlDeployment.metadata.namespace || '' + } if (!yamlDeployment.metadata || !yamlDeployment.metadata.name) { throw new Error(`Deployment read from ${filePath} must have name specified`) } @@ -1319,9 +1412,17 @@ export class KubeHelper { async createCheCluster(cheClusterCR: any, flags: any, ctx: any, useDefaultCR: boolean): Promise { const cheNamespace = flags.chenamespace if (useDefaultCR) { - // If we don't use an explicitly provided CheCluster CR, - // then let's modify the default example CR with values - // derived from the other parameters + // 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) @@ -1365,17 +1466,9 @@ export class KubeHelper { cheClusterCR.spec.storage.postgresPVCStorageClassName = flags['postgres-pvc-storage-class-name'] cheClusterCR.spec.storage.workspacePVCStorageClassName = flags['workspace-pvc-storage-class-name'] - if (flags.cheimage === DEFAULT_CHE_IMAGE && - cheClusterCR.spec.server.cheImageTag !== 'nightly' && - cheClusterCR.spec.server.cheImageTag !== 'latest') { - // We obviously are using a release version of chectl with the default `cheimage` - // => We should use the operator defaults for docker images - cheClusterCR.spec.server.cheImage = '' - cheClusterCR.spec.server.cheImageTag = '' - cheClusterCR.spec.server.pluginRegistryImage = '' - cheClusterCR.spec.server.devfileRegistryImage = '' - cheClusterCR.spec.auth.identityProviderImage = '' - } + // 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 @@ -2303,7 +2396,7 @@ export class KubeHelper { } public safeLoadFromYamlFile(filePath: string): any { - return yaml.safeLoad(fs.readFileSync(filePath).toString()) + return safeLoadFromYamlFile(filePath) } } diff --git a/src/api/version.ts b/src/api/version.ts index 39fca262b..147aa070e 100644 --- a/src/api/version.ts +++ b/src/api/version.ts @@ -8,14 +8,30 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ +import axios from 'axios' import execa = require('execa') +import * as fs from 'fs-extra' +import * as https from 'https' import Listr = require('listr') +import * as path from 'path' +import * as semver from 'semver' import { CheTasks } from '../tasks/che' -import { getClusterClientCommand } from '../util' +import { getClusterClientCommand, getProjectName, getProjectVersion } from '../util' +import { ChectlContext } from './context' import { KubeHelper } from './kube' +export const CHECTL_DEVELOPMENT_VERSION = '0.0.2' + +const UPDATE_INFO_FILENAME = 'update-info.json' +interface NewVersionInfoData { + latestVersion: string + // datetime of last check in milliseconds + lastCheck: number +} +const A_DAY_IN_MS = 24 * 60 * 60 * 1000 + export namespace VersionHelper { export const MINIMAL_OPENSHIFT_VERSION = '3.11' export const MINIMAL_K8S_VERSION = '1.9' @@ -39,7 +55,7 @@ export namespace VersionHelper { if (!flags['skip-version-check'] && actualVersion) { const checkPassed = checkMinimalVersion(actualVersion, MINIMAL_OPENSHIFT_VERSION) if (!checkPassed) { - throw getError('OpenShift', actualVersion, MINIMAL_OPENSHIFT_VERSION) + throw getMinimalVersionError(actualVersion, MINIMAL_OPENSHIFT_VERSION, 'OpenShift') } } } @@ -69,7 +85,7 @@ export namespace VersionHelper { if (!flags['skip-version-check'] && actualVersion) { const checkPassed = checkMinimalVersion(actualVersion, MINIMAL_K8S_VERSION) if (!checkPassed) { - throw getError('Kubernetes', actualVersion, MINIMAL_K8S_VERSION) + throw getMinimalVersionError(actualVersion, MINIMAL_K8S_VERSION, 'Kubernetes') } } } @@ -118,7 +134,7 @@ export namespace VersionHelper { return (actualMajor > minimalMajor || (actualMajor === minimalMajor && actualMinor >= minimalMinor)) } - export function getError(actualVersion: string, minimalVersion: string, component: string): Error { + export function getMinimalVersionError(actualVersion: string, minimalVersion: string, component: string): Error { return new Error(`The minimal supported version of ${component} is '${minimalVersion} but found '${actualVersion}'. To bypass version check use '--skip-version-check' flag.`) } @@ -163,7 +179,93 @@ export namespace VersionHelper { } } - function removeVPrefix(version: string): string { - return version.startsWith('v') ? version.substring(1) : version + /** + * Returns latest chectl version for the given channel. + */ + export async function getLatestChectlVersion(channel: string): Promise { + if (getProjectName() !== 'chectl') { + return + } + + const axiosInstance = axios.create({ + httpsAgent: new https.Agent({}) + }) + + try { + const { data } = await axiosInstance.get(`https://che-incubator.github.io/chectl/channels/${channel}/linux-x64`) + return data.version + } catch { + return + } } + + /** + * Checks whether there is an update available for current chectl. + */ + export async function isChectlUpdateAvailable(cacheDir: string, forceRecheck = false): Promise { + // Do not use ctx inside this function as the function is used from hook where ctx is not yet defined. + + if (getProjectName() !== 'chectl') { + // Do nothing for chectl flavors + return false + } + + const currentVersion = getProjectVersion() + if (currentVersion === CHECTL_DEVELOPMENT_VERSION) { + // Skip it, chectl is built from source + return false + } + + const channel = currentVersion.includes('next') ? 'next' : 'stable' + const newVersionInfoFilePath = path.join(cacheDir, `${channel}-${UPDATE_INFO_FILENAME}`) + let newVersionInfo: NewVersionInfoData = { + latestVersion: '0.0.0', + lastCheck: 0, + } + if (await fs.pathExists(newVersionInfoFilePath)) { + newVersionInfo = (await fs.readJson(newVersionInfoFilePath, { encoding: 'utf8' })) as NewVersionInfoData + } + + // Check cache, if it is already known that newer version available + const isCachedNewerVersionAvailable = semver.gt(newVersionInfo.latestVersion, currentVersion) + const now = Date.now() + const isCacheExpired = now - newVersionInfo.lastCheck > A_DAY_IN_MS + if (forceRecheck || (!isCachedNewerVersionAvailable && isCacheExpired)) { + // Cached info is expired. Fetch actual info about versions. + // undefined cannot be returned from getLatestChectlVersion as 'is flavor' check was done before. + const latestVersion = (await getLatestChectlVersion(channel))! + newVersionInfo = { latestVersion, lastCheck: now } + await fs.writeJson(newVersionInfoFilePath, newVersionInfo, { encoding: 'utf8' }) + return semver.gt(newVersionInfo.latestVersion, currentVersion) + } + + // Information whether a newer version available is already in cache + return isCachedNewerVersionAvailable + } + + /** + * Indicates if stable version of Eclipse Che is specified or meant implicitly. + */ + export function isDeployingStableVersion(flags: any): boolean { + return !!flags.version || !ChectlContext.get().isNightly + } + + /** + * Removes 'v' prefix from version string. + * @param version version to process + * @param checkForNumber if true remove prefix only if a numeric version follow it (e.g. v7.x -> 7.x, vNext -> vNext) + */ + export function removeVPrefix(version: string, checkForNumber = false): string { + if (version.startsWith('v') && version.length > 1) { + if (checkForNumber) { + const char2 = version.charAt(1) + if (char2 >= '0' && char2 <= '9') { + return version.substr(1) + } + } + return version.substr(1) + } + return version + } + } diff --git a/src/commands/server/delete.ts b/src/commands/server/delete.ts index 16db44d48..2fd32bfd3 100644 --- a/src/commands/server/delete.ts +++ b/src/commands/server/delete.ts @@ -15,12 +15,11 @@ import * as Listrq from 'listr' import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' -import { assumeYes, cheDeployment, cheNamespace, CHE_TELEMETRY, devWorkspaceControllerNamespace, listrRenderer, skipKubeHealthzCheck } from '../../common-flags' +import { assumeYes, batch, cheDeployment, cheNamespace, CHE_TELEMETRY, devWorkspaceControllerNamespace, listrRenderer, skipKubeHealthzCheck } from '../../common-flags' import { DEFAULT_ANALYTIC_HOOK_NAME } from '../../constants' import { CheTasks } from '../../tasks/che' import { DevWorkspaceTasks } from '../../tasks/component-installers/devfile-workspace-operator-installer' import { HelmTasks } from '../../tasks/installers/helm' -import { MinishiftAddonTasks } from '../../tasks/installers/minishift-addon' import { OLMTasks } from '../../tasks/installers/olm' import { OperatorTasks } from '../../tasks/installers/operator' import { ApiTasks } from '../../tasks/platforms/api' @@ -32,6 +31,7 @@ export default class Delete extends Command { static flags: flags.Input = { help: flags.help({ char: 'h' }), chenamespace: cheNamespace, + batch, 'dev-workspace-controller-namespace': devWorkspaceControllerNamespace, 'delete-namespace': boolean({ description: 'Indicates that a Eclipse Che namespace will be deleted as well', @@ -63,7 +63,6 @@ export default class Delete extends Command { const apiTasks = new ApiTasks() const helmTasks = new HelmTasks(flags) - const minishiftAddonTasks = new MinishiftAddonTasks() const operatorTasks = new OperatorTasks() const olmTasks = new OLMTasks() const cheTasks = new CheTasks(flags) @@ -77,13 +76,12 @@ export default class Delete extends Command { tasks.add(cheTasks.deleteTasks(flags)) tasks.add(devWorkspaceTasks.getUninstallTasks()) tasks.add(helmTasks.deleteTasks(flags)) - tasks.add(minishiftAddonTasks.deleteTasks(flags)) tasks.add(cheTasks.waitPodsDeletedTasks()) if (flags['delete-namespace']) { tasks.add(cheTasks.deleteNamespace(flags)) } - if (await this.isDeletionConfirmed(flags)) { + if (flags.batch || await this.isDeletionConfirmed(flags)) { try { await tasks.run() cli.log(getCommandSuccessMessage()) @@ -104,7 +102,7 @@ export default class Delete extends Command { throw new Error('Failed to get current Kubernetes cluster. Check if the current context is set via kubectl/oc') } - if (!flags.yes) { + if (!flags.batch && !flags.yes) { return cli.confirm(`You're going to remove Eclipse Che server in namespace '${flags.chenamespace}' on server '${cluster ? cluster.server : ''}'. If you want to continue - press Y`) } diff --git a/src/commands/server/deploy.ts b/src/commands/server/deploy.ts index 30c4e2a80..c6c5175d0 100644 --- a/src/commands/server/deploy.ts +++ b/src/commands/server/deploy.ts @@ -11,21 +11,20 @@ import { Command, flags } from '@oclif/command' import { boolean, string } from '@oclif/parser/lib/flags' import { cli } from 'cli-ux' -import * as fs from 'fs-extra' import * as Listr from 'listr' -import * as path from 'path' +import * as semver from 'semver' import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' -import { cheDeployment, cheNamespace, cheOperatorCRPatchYaml, cheOperatorCRYaml, CHE_OPERATOR_CR_PATCH_YAML_KEY, CHE_OPERATOR_CR_YAML_KEY, CHE_TELEMETRY, devWorkspaceControllerNamespace, k8sPodDownloadImageTimeout, K8SPODDOWNLOADIMAGETIMEOUT_KEY, k8sPodErrorRecheckTimeout, K8SPODERRORRECHECKTIMEOUT_KEY, k8sPodReadyTimeout, K8SPODREADYTIMEOUT_KEY, k8sPodWaitTimeout, K8SPODWAITTIMEOUT_KEY, listrRenderer, logsDirectory, LOG_DIRECTORY_KEY, skipKubeHealthzCheck as skipK8sHealthCheck } from '../../common-flags' -import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_CHE_NAMESPACE, DEFAULT_CHE_OPERATOR_IMAGE, DEFAULT_DEV_WORKSPACE_CONTROLLER_IMAGE, DEFAULT_OLM_SUGGESTED_NAMESPACE, DOCS_LINK_INSTALL_RUNNING_CHE_LOCALLY } from '../../constants' +import { batch, cheDeployment, cheDeployVersion, cheNamespace, cheOperatorCRPatchYaml, cheOperatorCRYaml, CHE_OPERATOR_CR_PATCH_YAML_KEY, CHE_OPERATOR_CR_YAML_KEY, CHE_TELEMETRY, DEPLOY_VERSION_KEY, devWorkspaceControllerNamespace, k8sPodDownloadImageTimeout, K8SPODDOWNLOADIMAGETIMEOUT_KEY, k8sPodErrorRecheckTimeout, K8SPODERRORRECHECKTIMEOUT_KEY, k8sPodReadyTimeout, K8SPODREADYTIMEOUT_KEY, k8sPodWaitTimeout, K8SPODWAITTIMEOUT_KEY, listrRenderer, logsDirectory, LOG_DIRECTORY_KEY, skipKubeHealthzCheck as skipK8sHealthCheck } from '../../common-flags' +import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_CHE_NAMESPACE, DEFAULT_DEV_WORKSPACE_CONTROLLER_IMAGE, DEFAULT_OLM_SUGGESTED_NAMESPACE, DOCS_LINK_INSTALL_RUNNING_CHE_LOCALLY, MIN_CHE_OPERATOR_INSTALLER_VERSION, MIN_HELM_INSTALLER_VERSION, MIN_OLM_INSTALLER_VERSION } from '../../constants' import { CheTasks } from '../../tasks/che' import { DevWorkspaceTasks } from '../../tasks/component-installers/devfile-workspace-operator-installer' -import { getPrintHighlightedMessagesTask, getRetrieveKeycloakCredentialsTask, retrieveCheCaCertificateTask } from '../../tasks/installers/common-tasks' +import { checkChectlAndCheVersionCompatibility, downloadTemplates, getPrintHighlightedMessagesTask, getRetrieveKeycloakCredentialsTask, retrieveCheCaCertificateTask } from '../../tasks/installers/common-tasks' import { InstallerTasks } from '../../tasks/installers/installer' import { ApiTasks } from '../../tasks/platforms/api' import { PlatformTasks } from '../../tasks/platforms/platform' -import { getCommandErrorMessage, getCommandSuccessMessage, isOpenshiftPlatformFamily, notifyCommandCompletedSuccessfully } from '../../util' +import { askForChectlUpdateIfNeeded, getCommandErrorMessage, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, getProjectName, isKubernetesPlatformFamily, isOpenshiftPlatformFamily, notifyCommandCompletedSuccessfully } from '../../util' export default class Deploy extends Command { static description = 'Deploy Eclipse Che server' @@ -33,6 +32,7 @@ export default class Deploy extends Command { static flags: flags.Input = { help: flags.help({ char: 'h' }), chenamespace: cheNamespace, + batch, 'listr-renderer': listrRenderer, 'deployment-name': cheDeployment, cheimage: string({ @@ -43,7 +43,8 @@ export default class Deploy extends Command { templates: string({ char: 't', description: 'Path to the templates folder', - env: 'CHE_TEMPLATES_FOLDER' + env: 'CHE_TEMPLATES_FOLDER', + exclusive: [DEPLOY_VERSION_KEY], }), 'devfile-registry-url': string({ description: 'The URL of the external Devfile registry.', @@ -93,7 +94,7 @@ export default class Deploy extends Command { installer: string({ char: 'a', description: 'Installer type. If not set, default is "olm" for OpenShift 4.x platform otherwise "operator".', - options: ['helm', 'operator', 'olm', 'minishift-addon'], + options: ['helm', 'operator', 'olm'], }), domain: string({ char: 'b', @@ -111,7 +112,6 @@ export default class Deploy extends Command { }), 'che-operator-image': string({ description: 'Container image of the operator. This parameter is used only when the installer is the operator', - default: DEFAULT_CHE_OPERATOR_IMAGE }), [CHE_OPERATOR_CR_YAML_KEY]: cheOperatorCRYaml, [CHE_OPERATOR_CR_PATCH_YAML_KEY]: cheOperatorCRPatchYaml, @@ -143,7 +143,6 @@ export default class Deploy extends Command { 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'.`, - default: true, allowNo: true, exclusive: ['starting-csv'] }), @@ -203,34 +202,34 @@ export default class Deploy extends Command { env: 'DEV_WORKSPACE_OPERATOR_IMAGE', }), 'dev-workspace-controller-namespace': devWorkspaceControllerNamespace, - telemetry: CHE_TELEMETRY + 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.templates) { - // use local templates folder if present - const templates = 'templates' - const templatesDir = path.resolve(templates) - if (flags.installer === 'operator') { - if (fs.pathExistsSync(`${templatesDir}/che-operator`)) { - flags.templates = templatesDir - } - } else if (flags.installer === 'minishift-addon') { - if (fs.pathExistsSync(`${templatesDir}/minishift-addon/`)) { - flags.templates = templatesDir - } - } + 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 (!flags.templates) { - flags.templates = path.join(__dirname, '../../../templates') - } + 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() } } @@ -252,7 +251,7 @@ export default class Deploy extends Command { return true } - checkPlatformCompatibility(flags: any) { + 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') @@ -274,51 +273,84 @@ export default class Deploy extends Command { this.warn('"--domain" flag is ignored for Openshift family infrastructures. It should be done on the cluster level.') } - if (flags.installer) { - if (flags.installer === 'minishift-addon') { - if (flags.platform !== 'minishift') { - this.error(`🛑 Current platform is ${flags.platform}. Minishift-addon is only available for Minishift.`) - } - } else if (flags.installer === 'helm') { - if (flags.platform !== 'k8s' && flags.platform !== 'minikube' && flags.platform !== 'microk8s' && 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 === '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' && flags.platform === 'minishift') { + 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.installer !== 'olm' && flags['starting-csv']) { + 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.installer !== 'olm' && flags['catalog-source-yaml']) { + if (flags['catalog-source-yaml']) { this.error('"catalog-source-yaml" flag should be used only with "olm" installer.') } - if (flags.installer !== 'olm' && flags['olm-channel']) { + if (flags['olm-channel']) { this.error('"olm-channel" flag should be used only with "olm" installer.') } - if (flags.installer !== 'olm' && flags['package-manifest-name']) { + if (flags['package-manifest-name']) { this.error('"package-manifest-name" flag should be used only with "olm" installer.') } - if (flags.installer !== 'olm' && flags['catalog-source-name']) { + if (flags['catalog-source-name']) { this.error('"catalog-source-name" flag should be used only with "olm" installer.') } - if (flags.installer !== 'olm' && flags['catalog-source-namespace']) { + if (flags['catalog-source-namespace']) { this.error('"package-manifest-name" flag should be used only with "olm" installer.') } - if (flags.installer !== 'olm' && flags['cluster-monitoring'] && flags.platform !== 'openshift') { + if (flags['cluster-monitoring'] && flags.platform !== 'openshift') { this.error('"cluster-monitoring" flag should be used only with "olm" installer and "openshift" platform.') } - 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['package-manifest-name'] && flags['catalog-source-yaml']) { - this.error('you need define "package-manifest-name" flag to use "catalog-source-yaml".') + 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' } - if (!flags['olm-channel'] && flags['catalog-source-yaml']) { - this.error('you need define "olm-channel" flag to use "catalog-source-yaml".') + + if (semver.gt(minAllowedVersion, flags.version)) { + throw new Error(`This chectl version can deploy ${minAllowedVersion} version and higher, but ${flags.version} is provided. If you really need to deploy that old version, please download corresponding legacy chectl version.`) } } } @@ -328,18 +360,13 @@ export default class Deploy extends Command { flags.chenamespace = flags.chenamespace || DEFAULT_CHE_NAMESPACE const ctx = await ChectlContext.initAndGet(flags, this) - if (flags['self-signed-cert']) { - this.warn('"self-signed-cert" flag is deprecated and has no effect. Autodetection is used instead.') + if (!flags.batch && ctx.isChectl) { + await askForChectlUpdateIfNeeded() } await this.setPlaformDefaults(flags, ctx) - - if (flags.installer === 'olm' && flags['olm-suggested-namespace'] && flags.chenamespace !== DEFAULT_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}.`) - } - 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() @@ -356,6 +383,8 @@ export default class Deploy extends Command { title: '👀 Looking for an already existing Eclipse Che instance', task: () => new Listr(cheTasks.checkIfCheIsInstalledTasks(flags, this)) }) + preInstallTasks.add(checkChectlAndCheVersionCompatibility(flags)) + preInstallTasks.add(downloadTemplates(flags)) let installTasks = new Listr(installerTasks.installTasks(flags, this), ctx.listrOptions) @@ -369,7 +398,6 @@ export default class Deploy extends Command { title: '🧪 DevWorkspace engine (experimental / technology preview) 🚨', enabled: () => flags['workspace-engine'] === 'dev-workspace', task: () => new Listr(devWorkspaceTasks.getInstallTasks(flags)) - }, getRetrieveKeycloakCredentialsTask(flags), retrieveCheCaCertificateTask(flags), @@ -392,7 +420,7 @@ export default class Deploy extends Command { } cli.warn(message) } else { - this.checkPlatformCompatibility(flags) + this.checkCompatibility(flags) await platformCheckTasks.run(ctx) await logsTasks.run(ctx) await installTasks.run(ctx) diff --git a/src/commands/server/update.ts b/src/commands/server/update.ts index 787750166..0128e2126 100644 --- a/src/commands/server/update.ts +++ b/src/commands/server/update.ts @@ -11,19 +11,18 @@ import { Command, flags } from '@oclif/command' import { string } from '@oclif/parser/lib/flags' import { cli } from 'cli-ux' -import * as fs from 'fs-extra' import * as Listr from 'listr' import { merge } from 'lodash' -import * as path from 'path' +import * as semver from 'semver' import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' -import { assumeYes, cheDeployment, cheNamespace, cheOperatorCRPatchYaml, CHE_OPERATOR_CR_PATCH_YAML_KEY, CHE_TELEMETRY, listrRenderer, skipKubeHealthzCheck } from '../../common-flags' -import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_CHE_OPERATOR_IMAGE, SUBSCRIPTION_NAME } from '../../constants' -import { getPrintHighlightedMessagesTask } from '../../tasks/installers/common-tasks' +import { assumeYes, batch, cheDeployment, cheDeployVersion, cheNamespace, cheOperatorCRPatchYaml, CHE_OPERATOR_CR_PATCH_YAML_KEY, CHE_TELEMETRY, DEPLOY_VERSION_KEY, listrRenderer, skipKubeHealthzCheck } from '../../common-flags' +import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_CHE_OPERATOR_IMAGE_NAME, MIN_CHE_OPERATOR_INSTALLER_VERSION, SUBSCRIPTION_NAME } from '../../constants' +import { checkChectlAndCheVersionCompatibility, downloadTemplates, getPrintHighlightedMessagesTask } from '../../tasks/installers/common-tasks' import { InstallerTasks } from '../../tasks/installers/installer' import { ApiTasks } from '../../tasks/platforms/api' -import { findWorkingNamespace, getCommandErrorMessage, getCommandSuccessMessage, getImageTag, getLatestChectlVersion, getProjectName, getProjectVersion, notifyCommandCompletedSuccessfully } from '../../util' +import { askForChectlUpdateIfNeeded, findWorkingNamespace, getCommandErrorMessage, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, getProjectName, getProjectVersion, notifyCommandCompletedSuccessfully } from '../../util' export default class Update extends Command { static description = 'Update Eclipse Che server.' @@ -51,15 +50,16 @@ export default class Update extends Command { hidden: true, }), chenamespace: cheNamespace, + batch, templates: string({ char: 't', description: 'Path to the templates folder', - default: Update.getTemplatesDir(), - env: 'CHE_TEMPLATES_FOLDER' + env: 'CHE_TEMPLATES_FOLDER', + exclusive: [DEPLOY_VERSION_KEY], }), 'che-operator-image': string({ description: 'Container image of the operator. This parameter is used only when the installer is the operator', - default: DEFAULT_CHE_OPERATOR_IMAGE + hidden: true, }), 'skip-version-check': flags.boolean({ description: 'Skip minimal versions check.', @@ -72,19 +72,8 @@ export default class Update extends Command { yes: assumeYes, help: flags.help({ char: 'h' }), [CHE_OPERATOR_CR_PATCH_YAML_KEY]: cheOperatorCRPatchYaml, - telemetry: CHE_TELEMETRY - } - - static getTemplatesDir(): string { - // return local templates folder if present - const TEMPLATES = 'templates' - const templatesDir = path.resolve(TEMPLATES) - const exists = fs.pathExistsSync(templatesDir) - if (exists) { - return TEMPLATES - } - // else use the location from modules - return path.join(__dirname, '../../../templates') + telemetry: CHE_TELEMETRY, + [DEPLOY_VERSION_KEY]: cheDeployVersion, } async run() { @@ -92,20 +81,46 @@ export default class Update extends Command { flags.chenamespace = await findWorkingNamespace(flags) const ctx = await ChectlContext.initAndGet(flags, this) + if (!flags.batch && ctx.isChectl) { + await askForChectlUpdateIfNeeded() + } + await this.setDomainFlag(flags) if (!flags.installer) { await this.setDefaultInstaller(flags) cli.info(`› Installer type is set to: '${flags.installer}'`) } + await this.config.runHook(DEFAULT_ANALYTIC_HOOK_NAME, { command: Update.id, flags }) - const kubeHelper = new KubeHelper(flags) + 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() + } + + if (flags.version) { + if (!ctx.isChectl) { + // Flavors of chectl should not use upstream repositories, so version flag is not appliable + this.error(`${getProjectName()} does not support '--version' flag.`) + } + if (flags.installer === 'olm') { + this.error(`'--${DEPLOY_VERSION_KEY}' flag is not supported for OLM installer. 'server:update' command automatically updates to the next available version.`) + } + + if (flags.installer === 'operator' && semver.gt(MIN_CHE_OPERATOR_INSTALLER_VERSION, flags.version)) { + throw new Error(this.getWrongVersionMessage(flags.version, MIN_CHE_OPERATOR_INSTALLER_VERSION)) + } + } + const installerTasks = new InstallerTasks() // pre update tasks const apiTasks = new ApiTasks() const preUpdateTasks = new Listr([], ctx.listrOptions) preUpdateTasks.add(apiTasks.testApiTasks(flags, this)) + preUpdateTasks.add(checkChectlAndCheVersionCompatibility(flags)) + preUpdateTasks.add(downloadTemplates(flags)) preUpdateTasks.add(installerTasks.preUpdateTasks(flags, this)) // update tasks @@ -121,58 +136,32 @@ export default class Update extends Command { try { await preUpdateTasks.run(ctx) - } catch (err) { - this.error(getCommandErrorMessage(err)) - } - if (flags.installer === 'operator') { - const existedOperatorImage = `${ctx.deployedCheOperatorImage}:${ctx.deployedCheOperatorTag}` - const newOperatorImage = `${ctx.newCheOperatorImage}:${ctx.newCheOperatorTag}` - cli.info(`Existed Eclipse Che operator: ${existedOperatorImage}.`) - cli.info(`New Eclipse Che operator : ${newOperatorImage}.`) - - const defaultOperatorImageTag = getImageTag(DEFAULT_CHE_OPERATOR_IMAGE) - const chectlChannel = defaultOperatorImageTag === 'nightly' ? 'next' : 'stable' - const currentChectlVersion = getProjectVersion() - const latestChectlVersion = await getLatestChectlVersion(chectlChannel) - const chectlName = getProjectName() - - // the same version is already installed - if (newOperatorImage === existedOperatorImage) { - if (chectlName === 'chectl' && latestChectlVersion) { - // suggest update chectl first - if (currentChectlVersion !== latestChectlVersion) { - cli.warn(`It is not possible to update Eclipse Che to a newer version -using the current '${currentChectlVersion}' version of chectl. Please, update 'chectl' -to a newer version '${latestChectlVersion}' with the command 'chectl update ${chectlChannel}' -and then try again.`) - } else if (!flags[CHE_OPERATOR_CR_PATCH_YAML_KEY]) { - // same version, no patch then nothing to update - cli.info('Eclipse Che is already up to date.') - this.exit(0) - } - } else { - // unknown project, no patch file then suggest to update - if (!flags[CHE_OPERATOR_CR_PATCH_YAML_KEY]) { - cli.warn(`It is not possible to update Eclipse Che to a newer version -using the current '${currentChectlVersion}' version of '${getProjectName()}'. -Please, update '${getProjectName()}' and then try again.`) - this.exit(0) - } + if (flags.installer === 'operator') { + if (!await this.checkAbilityToUpdateCheOperatorAndAskUser(flags)) { + // Exit + return } - // custom operator image is used - } else if (newOperatorImage !== DEFAULT_CHE_OPERATOR_IMAGE) { - cli.warn(`Eclipse Che operator deployment will be updated with the provided image, -but other Eclipse Che components will be updated to the ${defaultOperatorImageTag} version. -Consider removing '--che-operator-image' to update Eclipse Che operator to the same version.`) } + await this.checkComponentImages(flags) - if (!flags.yes && !await cli.confirm('If you want to continue - press Y')) { - cli.info('Update cancelled by user.') - this.exit(0) - } + await updateTasks.run(ctx) + await postUpdateTasks.run(ctx) + + this.log(getCommandSuccessMessage()) + } catch (err) { + this.error(getCommandErrorMessage(err)) } + notifyCommandCompletedSuccessfully() + } + + /** + * Tests if existing Che installation uses custom docker images. + * If so, asks user whether keep custom images or revert to default images and update them. + */ + private async checkComponentImages(flags: any): Promise { + const kubeHelper = new KubeHelper(flags) const cheCluster = await kubeHelper.getCheCluster(flags.chenamespace) if (cheCluster.spec.server.cheImage || cheCluster.spec.server.cheImageTag @@ -182,65 +171,167 @@ Consider removing '--che-operator-image' to update Eclipse Che operator to the s || cheCluster.spec.auth.identityProviderImage) { let imagesListMsg = '' - const crPatch = ctx[ChectlContext.CR_PATCH] || {} - if (cheCluster.spec.server.pluginRegistryImage - && (!crPatch.spec || !crPatch.spec.server || !crPatch.spec.server.pluginRegistryImage)) { + const resetImagesCrPatch: { [key: string]: any } = {} + if (cheCluster.spec.server.pluginRegistryImage) { imagesListMsg += `\n - Plugin registry image: ${cheCluster.spec.server.pluginRegistryImage}` - merge(crPatch, { spec: { server: { pluginRegistryImage: '' } } }) + merge(resetImagesCrPatch, { spec: { server: { pluginRegistryImage: '' } } }) } - if (cheCluster.spec.server.devfileRegistryImage - && (!crPatch.spec || !crPatch.spec.server || !crPatch.spec.server.devfileRegistryImage)) { + if (cheCluster.spec.server.devfileRegistryImage) { imagesListMsg += `\n - Devfile registry image: ${cheCluster.spec.server.devfileRegistryImage}` - merge(crPatch, { spec: { server: { devfileRegistryImage: '' } } }) + merge(resetImagesCrPatch, { spec: { server: { devfileRegistryImage: '' } } }) } - if (cheCluster.spec.server.postgresImage - && (!crPatch.spec || !crPatch.spec.database || !crPatch.spec.database.postgresImage)) { + if (cheCluster.spec.server.postgresImage) { imagesListMsg += `\n - Postgres image: ${cheCluster.spec.database.postgresImage}` - merge(crPatch, { spec: { database: { postgresImage: '' } } }) + merge(resetImagesCrPatch, { spec: { database: { postgresImage: '' } } }) } - if (cheCluster.spec.server.identityProviderImage - && (!crPatch.spec || !crPatch.spec.auth || !crPatch.spec.auth.identityProviderImage)) { + if (cheCluster.spec.server.identityProviderImage) { imagesListMsg += `\n - Identity provider image: ${cheCluster.spec.auth.identityProviderImage}` - merge(crPatch, { spec: { auth: { identityProviderImage: '' } } }) + merge(resetImagesCrPatch, { spec: { auth: { identityProviderImage: '' } } }) } - if (cheCluster.spec.server.cheImage - && (!crPatch.spec || !crPatch.spec.server || !crPatch.spec.server.cheImage)) { + if (cheCluster.spec.server.cheImage) { imagesListMsg += `\n - Eclipse Che server image name: ${cheCluster.spec.server.cheImage}` - merge(crPatch, { spec: { server: { cheImage: '' } } }) + merge(resetImagesCrPatch, { spec: { server: { cheImage: '' } } }) } - if (cheCluster.spec.server.cheImageTag - && (!crPatch.spec || !crPatch.spec.server || !crPatch.spec.server.cheImageTag)) { + if (cheCluster.spec.server.cheImageTag) { imagesListMsg += `\n - Eclipse Che server image tag: ${cheCluster.spec.server.cheImageTag}` - merge(crPatch, { spec: { server: { cheImageTag: '' } } }) + merge(resetImagesCrPatch, { spec: { server: { cheImageTag: '' } } }) } - ctx[ChectlContext.CR_PATCH] = crPatch if (imagesListMsg) { - cli.warn(`In order to update Eclipse Che to a newer version the fields defining the images in the '${cheCluster.metadata.name}' -Custom Resource in the '${flags.chenamespace}' namespace will be cleaned up:${imagesListMsg}`) - if (!flags.yes && !await cli.confirm('If you want to continue - press Y')) { - cli.info('Update cancelled by user.') - this.exit(0) + cli.warn(`Custom images found in '${cheCluster.metadata.name}' Custom Resource in the '${flags.chenamespace}' namespace: ${imagesListMsg}`) + if (flags.batch || flags.yes || await cli.confirm('Do you want to preserve custom images [y/n]?')) { + cli.info('Keeping current images.\nNote, Update might fail if functionality of the custom images different from the default ones.') + } else { + cli.info('Resetting custom images to default ones.') + + const ctx = ChectlContext.get() + const crPatch = ctx[ChectlContext.CR_PATCH] || {} + merge(crPatch, resetImagesCrPatch) + ctx[ChectlContext.CR_PATCH] = crPatch } } } + } - try { - await updateTasks.run(ctx) - await postUpdateTasks.run(ctx) + /** + * Check whether chectl should proceed with update. + * Asks user for confirmation (unless assume yes is provided). + * Is applicable to operator installer only. + * Returns true if chectl can/should proceed with update, false otherwise. + */ + private async checkAbilityToUpdateCheOperatorAndAskUser(flags: any): Promise { + const ctx = ChectlContext.get() + cli.info(`Existing Eclipse Che operator: ${ctx.deployedCheOperatorImage}`) + cli.info(`New Eclipse Che operator : ${ctx.newCheOperatorImage}`) - this.log(getCommandSuccessMessage()) - } catch (err) { - this.error(getCommandErrorMessage(err)) + if (ctx.deployedCheOperatorImageName === DEFAULT_CHE_OPERATOR_IMAGE_NAME && ctx.newCheOperatorImageName === DEFAULT_CHE_OPERATOR_IMAGE_NAME) { + // Official images + + if (ctx.deployedCheOperatorImage === ctx.newCheOperatorImage) { + if (ctx.newCheOperatorImageTag === 'nightly') { + cli.info('Updating current Eclipse Che nightly version to a new one.') + return true + } + + if (flags[CHE_OPERATOR_CR_PATCH_YAML_KEY]) { + // 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 + } + } + + if (this.isUpgrade(ctx.deployedCheOperatorImageTag, ctx.newCheOperatorImageTag)) { + // Upgrade + + const currentChectlVersion = getProjectVersion() + if (!ctx.isNightly && (ctx.newCheOperatorImageTag === 'nightly' || semver.lt(currentChectlVersion, ctx.newCheOperatorImageTag))) { + // Upgrade is not allowed + if (ctx.newCheOperatorImageTag === 'nightly') { + cli.warn(`Stable ${getProjectName()} cannot update stable Eclipse Che to nightly version`) + } else { + cli.warn(`It is not possible to update Eclipse Che to a newer version using the current '${currentChectlVersion}' version of chectl. Please, update '${getProjectName()}' to a newer version using command '${getProjectName()} update' and then try again.`) + } + return false + } + + // Upgrade allowed + if (ctx.newCheOperatorImageTag === 'nightly') { + cli.info(`You are going to update Eclipse Che ${ctx.deployedCheOperatorImageTag} to nightly version.`) + } else { + cli.info(`You are going to update Eclipse Che ${ctx.deployedCheOperatorImageTag} to ${ctx.newCheOperatorImageTag}`) + } + } else { + // Downgrade + + if (semver.gt(MIN_CHE_OPERATOR_INSTALLER_VERSION, flags.version)) { + cli.info(`Given Eclipse Che version ${flags.version} is too old to be downgraded to`) + return false + } + + cli.info(`You are going to downgrade Eclipse Che ${ctx.deployedCheOperatorImageTag} to ${ctx.newCheOperatorImageTag}`) + cli.warn('DOWNGRADE IS NOT OFFICIALLY SUPPORTED, PROCEED ON YOUR OWN RISK') + } + } else { + // At least one of the images is custom + + // Print message + if (ctx.deployedCheOperatorImage === ctx.newCheOperatorImage) { + // Despite the image is the same it could be updated image, replace anyway. + cli.info(`You are going to replace Eclipse Che operator image ${ctx.newCheOperatorImage}.`) + } else if (ctx.deployedCheOperatorImageName !== DEFAULT_CHE_OPERATOR_IMAGE_NAME && ctx.newCheOperatorImageName !== DEFAULT_CHE_OPERATOR_IMAGE_NAME) { + // Both images are custom + cli.info(`You are going to update ${ctx.deployedCheOperatorImage} to ${ctx.newCheOperatorImage}`) + } else { + // One of the images is offical + if (ctx.deployedCheOperatorImageName === DEFAULT_CHE_OPERATOR_IMAGE_NAME) { + // Update from offical to custom image + cli.info(`You are going to update official ${ctx.deployedCheOperatorImage} image with user provided one: ${ctx.newCheOperatorImage}`) + } else { // ctx.newCheOperatorImageName === DEFAULT_CHE_OPERATOR_IMAGE_NAME + // Update from custom to official image + cli.info(`You are going to update user provided image ${ctx.deployedCheOperatorImage} with official one: ${ctx.newCheOperatorImage}`) + } + } } - notifyCommandCompletedSuccessfully() - this.exit(0) + if (!flags.batch && !flags.yes && !await cli.confirm('If you want to continue - press Y')) { + cli.info('Update cancelled by user.') + return false + } + + return true + } + + /** + * Checks if official operator image is replaced with a newer one. + * Tags are allowed in format x.y.z or nightly. + * nightly is considered the most recent. + * For example: + * (7.22.1, 7.23.0) -> true, + * (7.22.1, 7.20.2) -> false, + * (7.22.1, nightly) -> true, + * (nightly, 7.20.2) -> false + * @param oldTag old official operator image tag, e.g. 7.20.1 + * @param newTag new official operator image tag e.g. 7.22.0 + * @returns true if upgrade, false if downgrade + * @throws error if tags are equal + */ + private isUpgrade(oldTag: string, newTag: string): boolean { + if (oldTag === newTag) { + throw new Error(`Tags are the same: ${newTag}`) + } + // if newTag is nightly it is upgrade + // if oldTag is nightly it is downgrade + // otherwise just compare new and old tags + // Note, that semver lib doesn't handle text tags and throws an error in case nightly is provided for comparation. + return newTag === 'nightly' || (oldTag !== 'nightly' && semver.gt(newTag, oldTag)) } /** @@ -266,4 +357,9 @@ Custom Resource in the '${flags.chenamespace}' namespace will be cleaned up:${im flags.installer = 'operator' } } + + private getWrongVersionMessage(current: string, minimal: string): string { + return `This chectl version can deploy ${minimal} version and higher, but ${current} is provided. If you really need to deploy that old version, please download corresponding legacy chectl version.` + } + } diff --git a/src/common-flags.ts b/src/common-flags.ts index a07f971fc..1618b821a 100644 --- a/src/common-flags.ts +++ b/src/common-flags.ts @@ -23,6 +23,12 @@ export const devWorkspaceControllerNamespace = string({ env: 'DEV_WORKSPACE_OPERATOR_NAMESPACE', }) +export const batch = boolean({ + description: 'Batch mode. Running a command without end user interaction.', + default: false, + required: false, +}) + export const cheDeployment = string({ description: 'Eclipse Che deployment name', default: 'che', @@ -65,6 +71,7 @@ export const assumeYes = boolean({ char: 'y', default: false, required: false, + exclusive: ['batch'], }) export const CHE_OPERATOR_CR_YAML_KEY = 'che-operator-cr-yaml' @@ -116,3 +123,10 @@ export const CHE_TELEMETRY = string({ description: 'Enable or disable telemetry. This flag skips a prompt and enable/disable telemetry', options: ['on', 'off'] }) + +export const DEPLOY_VERSION_KEY = 'version' +export const cheDeployVersion = string({ + char: 'v', + description: 'Version to deploy (e.g. 7.15.2). Defaults to the same as chectl.', + env: 'CHE_DEPLOY_VERSION', +}) diff --git a/src/constants.ts b/src/constants.ts index 01e9d13e5..583a40f29 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,12 +8,16 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ +// minimal installable versions by current chectl +export const MIN_OLM_INSTALLER_VERSION = '7.17.0' +export const MIN_CHE_OPERATOR_INSTALLER_VERSION = '7.13.1' +export const MIN_HELM_INSTALLER_VERSION = '7.10.0' + // labels export const CHE_RELATED_COMPONENT_LABEL = 'client/org.eclipse.che=true' // images -export const DEFAULT_CHE_IMAGE = 'quay.io/eclipse/che-server:nightly' -export const DEFAULT_CHE_OPERATOR_IMAGE = 'quay.io/eclipse/che-operator:nightly' +export const DEFAULT_CHE_OPERATOR_IMAGE_NAME = 'quay.io/eclipse/che-operator' export const DEFAULT_DEV_WORKSPACE_CONTROLLER_IMAGE = 'quay.io/devfile/devworkspace-controller:next' // This image should be updated manually when needed. // Repository location: https://github.com/che-dockerfiles/che-cert-manager-ca-cert-generator-image diff --git a/src/hooks/prerun/new-version-warning.ts b/src/hooks/prerun/new-version-warning.ts new file mode 100644 index 000000000..59b061f48 --- /dev/null +++ b/src/hooks/prerun/new-version-warning.ts @@ -0,0 +1,37 @@ +/********************************************************************* + * Copyright (c) 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/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ + +import { Hook } from '@oclif/config' +import { cli } from 'cli-ux' + +import { VersionHelper } from '../../api/version' +import { getProjectName } from '../../util' + +const isChectl = getProjectName() === 'chectl' +const hook: Hook<'prerun'> = async function (options) { + if (!isChectl) { + return + } + + const commandName: string = options.Command.id + if (commandName === 'server:deploy' || commandName === 'server:update') { + return + } + + try { + if (await VersionHelper.isChectlUpdateAvailable(options.config.cacheDir)) { + cli.warn('A newer version of chectl is available. Run "chectl update" to update chectl to the newer version.') + } + } catch { + // An error occured while checking for newer version. Ignore it. + } +} + +export default hook diff --git a/src/tasks/component-installers/cert-manager.ts b/src/tasks/component-installers/cert-manager.ts index 47adac1e7..b74ce7f82 100644 --- a/src/tasks/component-installers/cert-manager.ts +++ b/src/tasks/component-installers/cert-manager.ts @@ -8,6 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ +import * as fs from 'fs-extra' import * as Listr from 'listr' import * as path from 'path' @@ -51,7 +52,12 @@ export class CertManagerTasks { title: 'Deploy cert-manager', enabled: ctx => !ctx.certManagerInstalled, task: async (ctx: any, task: any) => { - const yamlPath = path.join(flags.templates, '..', 'installers', 'cert-manager.yml') + let yamlPath = path.join(flags.templates, 'cert-manager', 'cert-manager.yaml') + if (! await fs.pathExists(yamlPath)) { + // Older Che versions don't have Cert Manager install yaml in templates + // Try to use embedded in chectl version + yamlPath = path.join(__dirname, '../../../installers/cert-manager.yml') + } // Apply additional --validate=false flag to be able to deploy Cert Manager on Kubernetes v1.15.4 or below await this.kubeHelper.applyResource(yamlPath, '--validate=false') ctx.certManagerInstalled = true diff --git a/src/tasks/installers/common-tasks.ts b/src/tasks/installers/common-tasks.ts index 552567623..71405bdd0 100644 --- a/src/tasks/installers/common-tasks.ts +++ b/src/tasks/installers/common-tasks.ts @@ -10,15 +10,19 @@ import Command from '@oclif/command' import ansi = require('ansi-colors') -import { copy, mkdirp, remove } from 'fs-extra' +import * as fs from 'fs-extra' import * as Listr from 'listr' import { isEmpty } from 'lodash' import * as path from 'path' +import * as semver from 'semver' import { CheHelper } from '../../api/che' import { ChectlContext } from '../../api/context' +import { CheGithubClient } from '../../api/github-client' import { KubeHelper } from '../../api/kube' +import { VersionHelper } from '../../api/version' import { CHE_CLUSTER_CRD, DOCS_LINK_IMPORT_CA_CERT_INTO_BROWSER } from '../../constants' +import { getProjectVersion } from '../../util' export function createNamespaceTask(namespaceName: string, labels: {}): Listr.ListrTask { return { @@ -40,25 +44,73 @@ export function createNamespaceTask(namespaceName: string, labels: {}): Listr.Li } } -export function copyOperatorResources(flags: any, cacheDir: string): Listr.ListrTask { +export function checkChectlAndCheVersionCompatibility(flags: any): Listr.ListrTask { return { - title: 'Copying operator resources', + title: 'Check versions compatibility', + enabled: ctx => ctx.isChectl && !flags.templates && flags.version && flags.installer !== 'olm', task: async (ctx: any, task: any) => { - ctx.resourcesPath = await copyCheOperatorResources(flags.templates, cacheDir) - task.title = `${task.title}...done.` + const githubClient = new CheGithubClient() + const verInfo = await githubClient.getTemplatesTagInfo(flags.installer, flags.version) + if (!verInfo) { + throw new Error(`Version ${flags.version} does not exist`) + } + ctx.versionInfo = verInfo + flags.version = VersionHelper.removeVPrefix(verInfo.name, true) + + if (!ctx.isNightly && semver.lt(getProjectVersion(), flags.version)) { + throw new Error(`To deploy Eclipse Che ${flags.version} please update your chectl first by running "chectl update" command`) + } + + task.title = `${task.title}... OK` } } } -async function copyCheOperatorResources(templatesDir: string, cacheDir: string): Promise { - const srcDir = path.join(templatesDir, '/che-operator/') - const destDir = path.join(cacheDir, '/templates/che-operator/') +/** + * Sets flags.templates based on required version and installer. + * Does not support OLM. + */ +export function downloadTemplates(flags: any): Listr.ListrTask { + return { + title: 'Download templates', + enabled: ctx => ctx.isChectl && flags.version && !flags.templates && flags.installer !== 'olm', + task: async (ctx: any, task: any) => { + // All templates are stored in the cache directory + // Example path: ~/.cache/chectl/templates/7.15.1/ + const templatesRootDir = path.join(ctx[ChectlContext.CACHE_DIR], 'templates') + + let installerTemplatesSubDir: string + switch (flags.installer) { + case 'operator': + installerTemplatesSubDir = 'che-operator' + break + case 'helm': + installerTemplatesSubDir = 'kubernetes' + break + case 'olm': + // Should be handled on install phase when catalog source is deployed + return + default: + throw new Error(`Unknow installer ${flags.installer}`) + } + + const versionTemplatesDirPath = path.join(templatesRootDir, flags.version) + flags.templates = versionTemplatesDirPath - await remove(destDir) - await mkdirp(destDir) - await copy(srcDir, destDir) + const installerTemplatesDirPath = path.join(versionTemplatesDirPath, installerTemplatesSubDir) + if (await fs.pathExists(installerTemplatesDirPath)) { + // Use cached templates + task.title = `${task.title}... found cached templates for version ${flags.version}` + return + } - return destDir + // Download templates + task.title = `${task.title} for version ${flags.version}` + const cheHelper = new CheHelper(flags) + await cheHelper.downloadAndUnpackTemplates(flags.installer, ctx.versionInfo.zipball_url, versionTemplatesDirPath) + task.title = `${task.title} ... OK` + } + } } export function createEclipseCheCluster(flags: any, kube: KubeHelper): Listr.ListrTask { diff --git a/src/tasks/installers/helm.ts b/src/tasks/installers/helm.ts index d63ba4236..c295af1a8 100644 --- a/src/tasks/installers/helm.ts +++ b/src/tasks/installers/helm.ts @@ -12,15 +12,22 @@ import { Command } from '@oclif/command' import * as commandExists from 'command-exists' import * as execa from 'execa' import * as fs from 'fs' -import { copy, mkdirp, remove } from 'fs-extra' import * as Listr from 'listr' import * as path from 'path' +import * as semver from 'semver' import { KubeHelper } from '../../api/kube' import { VersionHelper } from '../../api/version' -import { CHE_ROOT_CA_SECRET_NAME, CHE_TLS_SECRET_NAME, DEFAULT_CHE_IMAGE } from '../../constants' +import { CHE_ROOT_CA_SECRET_NAME, CHE_TLS_SECRET_NAME } from '../../constants' import { CertManagerTasks } from '../../tasks/component-installers/cert-manager' -import { generatePassword, isStableVersion } from '../../util' +import { generatePassword, safeSaveYamlToFile } from '../../util' + +interface HelmChartDependency { + name: string + repository: string + version: string + condition: string +} export class HelmTasks { protected kubeHelper: KubeHelper @@ -32,8 +39,8 @@ export class HelmTasks { /** * Returns list of tasks which perform preflight platform checks. */ - startTasks(flags: any, command: Command): Listr { - if (isStableVersion(flags)) { + deployTasks(flags: any, command: Command): Listr { + if (VersionHelper.isDeployingStableVersion(flags)) { command.warn('Consider using the more reliable \'OLM\' installer when deploying a stable release of Eclipse Che (--installer=olm).') } return new Listr([ @@ -51,7 +58,7 @@ export class HelmTasks { if (!flags['skip-version-check']) { const checkPassed = VersionHelper.checkMinimalHelmVersion(version) if (!checkPassed) { - throw VersionHelper.getError(version, VersionHelper.MINIMAL_HELM_VERSION, 'helm') + throw VersionHelper.getMinimalVersionError(version, VersionHelper.MINIMAL_HELM_VERSION, 'helm') } } @@ -76,7 +83,7 @@ export class HelmTasks { { title: 'Check Eclipse Che TLS certificate', task: async (ctx: any, task: any) => { - const fixErrorMessage = 'Helm installer generates secrets automatically. To fix the problem delete existed secrets in dedicated for Eclispe Che namespace and rerun the command.' + const fixErrorMessage = 'Helm installer generates secrets automatically. To fix the problem delete existed secrets in dedicated for Eclipse Che namespace and rerun the command.' const cheTlsSecret = await this.kubeHelper.getSecret(CHE_TLS_SECRET_NAME, flags.chenamespace) if (cheTlsSecret) { @@ -168,24 +175,22 @@ export class HelmTasks { } } }, - { - title: 'Preparing Eclipse Che Helm Chart', - task: async (_ctx: any, task: any) => { - await this.prepareCheHelmChart(flags, command.config.cacheDir) - task.title = `${task.title}...done.` - } - }, { title: 'Updating Helm Chart dependencies', task: async (_ctx: any, task: any) => { - await this.updateCheHelmChartDependencies(command.config.cacheDir) + if (flags.version && semver.gt('7.23.2', flags.version)) { + // Current version is below 7.23.2 + // Fix moved external depenency + await this.pathcCheHelmChartPrometheusAndGrafanaDependencies(flags) + } + await this.updateCheHelmChartDependencies(flags) task.title = `${task.title}...done.` } }, { title: 'Deploying Eclipse Che Helm Chart', task: async (ctx: any, task: any) => { - await this.upgradeCheHelmChart(ctx, flags, command.config.cacheDir) + await this.upgradeCheHelmChart(ctx, flags) task.title = `${task.title}...done.` } }, @@ -281,21 +286,32 @@ error: E_COMMAND_FAILED`) await execa('helm', ['delete', name, '--purge', '--namespace', namespace], { timeout: execTimeout, reject: false }) } - private async prepareCheHelmChart(flags: any, cacheDir: string) { - const srcDir = path.join(flags.templates, '/kubernetes/helm/che/') - const destDir = path.join(cacheDir, '/templates/kubernetes/helm/che/') - await remove(destDir) - await mkdirp(destDir) - await copy(srcDir, destDir) + private async pathcCheHelmChartPrometheusAndGrafanaDependencies(flags: any): Promise { + const helmChartDependenciesYamlPath = path.join(flags.templates, 'kubernetes', 'helm', 'che', 'requirements.yaml') + const helmChartDependenciesYaml = this.kubeHelper.safeLoadFromYamlFile(helmChartDependenciesYamlPath) + const deps: HelmChartDependency[] = helmChartDependenciesYaml && helmChartDependenciesYaml.dependencies || [] + let shouldReplaceYamlFile = false + for (const dep of deps) { + if (dep.name === 'prometheus' && dep.repository.startsWith('https://kubernetes-charts.storage.googleapis.com')) { + dep.repository = 'https://prometheus-community.github.io/helm-charts' + shouldReplaceYamlFile = true + } else if (dep.name === 'grafana' && dep.repository.startsWith('https://kubernetes-charts.storage.googleapis.com')) { + dep.repository = 'https://grafana.github.io/helm-charts' + shouldReplaceYamlFile = true + } + } + if (shouldReplaceYamlFile) { + safeSaveYamlToFile(helmChartDependenciesYaml, helmChartDependenciesYamlPath) + } } - private async updateCheHelmChartDependencies(cacheDir: string, execTimeout = 120000) { - const destDir = path.join(cacheDir, '/templates/kubernetes/helm/che/') + private async updateCheHelmChartDependencies(flags: any, execTimeout = 120000) { + const destDir = path.join(flags.templates, 'kubernetes', 'helm', 'che') await execa(`helm dependencies update ${destDir}`, { timeout: execTimeout, shell: true }) } - private async upgradeCheHelmChart(ctx: any, flags: any, cacheDir: string, execTimeout = 120000) { - const destDir = path.join(cacheDir, '/templates/kubernetes/helm/che/') + private async upgradeCheHelmChart(ctx: any, flags: any, execTimeout = 120000) { + const destDir = path.join(flags.templates, '/kubernetes/helm/che/') let multiUserFlag = '' let tlsFlag = '' @@ -343,9 +359,11 @@ error: E_COMMAND_FAILED`) setOptions.push(`--set che-keycloak.keycloakAdminUserPassword=${ctx.identityProviderPassword}`) } + if (flags.cheimage) { + setOptions.push(`--set cheImage=${flags.cheimage}`) + } + setOptions.push(`--set global.ingressDomain=${flags.domain}`) - const cheImage = flags.cheimage || DEFAULT_CHE_IMAGE - setOptions.push(`--set cheImage=${cheImage}`) setOptions.push(`--set che.disableProbes=${flags.debug}`) const patchFlags = flags['helm-patch-yaml'] ? '-f ' + flags['helm-patch-yaml'] : '' diff --git a/src/tasks/installers/installer.ts b/src/tasks/installers/installer.ts index 77f50ae3d..f1d8fbb0c 100644 --- a/src/tasks/installers/installer.ts +++ b/src/tasks/installers/installer.ts @@ -12,7 +12,6 @@ import Command from '@oclif/command' import * as Listr from 'listr' import { HelmTasks } from './helm' -import { MinishiftAddonTasks } from './minishift-addon' import { OLMTasks } from './olm' import { OperatorTasks } from './operator' @@ -27,7 +26,6 @@ export class InstallerTasks { let title: string let task: any - // let task: Listr.ListrTask if (flags.installer === 'operator') { title = '🏃‍ Running the Eclipse Che operator Update' task = () => { @@ -56,7 +54,6 @@ export class InstallerTasks { let title: string let task: any - // let task: Listr.ListrTask if (flags.installer === 'operator') { title = '🏃‍ Running the Eclipse Che operator Update' task = () => { @@ -82,13 +79,10 @@ export class InstallerTasks { const helmTasks = new HelmTasks(flags) const operatorTasks = new OperatorTasks() const olmTasks = new OLMTasks() - const minishiftAddonTasks = new MinishiftAddonTasks() let title: string let task: any - // let task: Listr.ListrTask - if (flags.installer === 'operator') { title = '🏃‍ Running the Eclipse Che operator' task = () => { @@ -97,7 +91,7 @@ export class InstallerTasks { flags.multiuser = true } - return operatorTasks.startTasks(flags, command) + return operatorTasks.deployTasks(flags, command) } } else if (flags.installer === 'olm') { title = '🏃‍ Running Olm installaion Eclipse Che' @@ -109,15 +103,7 @@ export class InstallerTasks { // installer.ts BEGIN CHE ONLY } else if (flags.installer === 'helm') { title = '🏃‍ Running Helm to install Eclipse Che' - task = () => helmTasks.startTasks(flags, command) - } else if (flags.installer === 'minishift-addon') { - // minishift-addon supports Eclipse Che singleuser only - if (flags.multiuser) { - command.warn("Eclipse Che will be deployed in Single-User mode as 'minishift-addon' installer supports only that mode.") - flags.multiuser = false - } - title = '🏃‍ Running the Eclipse Che minishift-addon' - task = () => minishiftAddonTasks.startTasks(flags, command) + task = () => helmTasks.deployTasks(flags, command) // installer.ts END CHE ONLY } else { title = '🏃‍ Installer preflight check' diff --git a/src/tasks/installers/minishift-addon.ts b/src/tasks/installers/minishift-addon.ts deleted file mode 100644 index 685756559..000000000 --- a/src/tasks/installers/minishift-addon.ts +++ /dev/null @@ -1,200 +0,0 @@ -/********************************************************************* - * Copyright (c) 2019 Red Hat, Inc. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - **********************************************************************/ - -import Command from '@oclif/command' -import * as commandExists from 'command-exists' -import * as execa from 'execa' -import { copy, mkdirp, remove } from 'fs-extra' -import * as Listr from 'listr' -import * as path from 'path' - -import { OpenShiftHelper } from '../../api/openshift' -import { DEFAULT_CHE_IMAGE } from '../../constants' - -export class MinishiftAddonTasks { - /** - * Returns list of tasks which perform preflight platform checks. - */ - startTasks(flags: any, command: Command): Listr { - let resourcesPath = '' - - return new Listr([ - { - title: 'Check minishift version', - task: async (_ctx: any, task: any) => { - const version = await this.grabVersion() - if (version < 133) { - command.error('The Eclipse Che minishift-addon requires minishift version >= 1.33.0. Please update your minishift installation with "minishift update" command.') - } - task.title = `${task.title}...done.` - } - }, - { - title: 'Check logged', - task: async (_ctx: any, task: any) => { - await this.checkLogged(command) - task.title = `${task.title}...done.` - } - }, - { - title: 'Copying addon resources', - task: async (_ctx: any, task: any) => { - resourcesPath = await this.copyResources(flags.templates, command.config.cacheDir) - task.title = `${task.title}...done.` - } - }, - { - title: 'Check Eclipse Che addon is available', - task: async (_ctx: any, task: any) => { - await this.installAddonIfMissing(resourcesPath) - task.title = `${task.title}...done.` - } - }, - { - title: 'Apply Eclipse Che addon', - task: async (ctx: any, task: any) => { - await this.applyAddon(flags) - ctx.isCheDeployed = true - ctx.isPluginRegistryDeployed = true - ctx.isDevfileRegistryDeployed = true - task.title = `${task.title}...done.` - } - } - ], { renderer: flags['listr-renderer'] as any }) - } - - /** - * Returns list of tasks which perform removing of addon if minishift is found. - */ - deleteTasks(_flags: any): ReadonlyArray { - return [{ - title: 'Remove Eclipse Che minishift-addon', - enabled: (ctx: any) => ctx.isOpenShift, - task: async (_ctx: any, task: any) => { - if (!commandExists.sync('minishift')) { - task.title = await `${task.title}...OK (minishift not found)` - } else { - await this.removeAddon() - task.title = await `${task.title}...OK` - } - } - } - ] - } - - async removeAddon(execTimeout = 120000) { - let args = ['addon', 'remove', 'che'] - await execa('minishift', args, { timeout: execTimeout, reject: false }) - } - - getImageRepository(image: string): string { - if (image.includes(':')) { - return image.split(':')[0] - } else { - return image - } - } - - getImageTag(image: string) { - if (image.includes(':')) { - return image.split(':')[1] - } else { - return 'latest' - } - } - - async grabVersion(): Promise { - let args = ['version'] - const { stdout } = await execa('minishift', - args, - { reject: false }) - if (stdout) { - return parseInt(stdout.replace(/\D/g, '').substring(0, 3), 10) - } - return -1 - - } - - private async checkLogged(command: Command) { - const openshiftHelper = new OpenShiftHelper() - const ok = await openshiftHelper.isOpenShiftRunning() - if (!ok) { - command.error('Not logged with OC tool. Please log-in with oc login command') - } - } - - private async installAddonIfMissing(resourcesPath: string) { - let args = ['addon', 'list'] - const { stdout } = await execa('minishift', - args, - { reject: false }) - if (stdout && stdout.includes('- che')) { - // needs to delete before installing - await this.uninstallAddon() - } - - // now install - const addonDir = path.join(resourcesPath, 'che') - await this.installAddon(addonDir) - - } - - private async applyAddon(flags: any, execTimeout = 120000) { - let args = ['addon', 'apply'] - const cheImage = flags.cheimage || DEFAULT_CHE_IMAGE - const imageRepo = this.getImageRepository(cheImage) - const imageTag = this.getImageTag(cheImage) - args = args.concat(['--addon-env', `NAMESPACE=${flags.chenamespace}`]) - args = args.concat(['--addon-env', `CHE_IMAGE_REPO=${imageRepo}`]) - args = args.concat(['--addon-env', `CHE_IMAGE_TAG=${imageTag}`]) - args = args.concat(['--addon-env', `PLUGIN_REGISTRY_TAG=${imageTag}`]) - args = args.concat(['--addon-env', `DEVFILE_REGISTRY_TAG=${imageTag}`]) - args = args.concat(['che']) - const { command, - exitCode, - stderr, - stdout, - timedOut } = await execa('minishift', - args, - { timeout: execTimeout, reject: false }) - if (timedOut) { - throw new Error(`Command "${command}" timed out after ${execTimeout}ms -stderr: ${stderr} -stdout: ${stdout} -error: E_TIMEOUT`) - } - if (exitCode !== 0) { - throw new Error(`Command "${command}" failed with return code ${exitCode} -stderr: ${stderr} -stdout: ${stdout} -error: E_COMMAND_FAILED`) - } - } - - private async installAddon(directory: string, execTimeout = 120000) { - let args = ['addon', 'install', directory] - await execa('minishift', args, { timeout: execTimeout }) - } - - private async uninstallAddon(execTimeout = 120000) { - let args = ['addon', 'uninstall', 'che'] - await execa('minishift', args, { timeout: execTimeout }) - } - - private async copyResources(templatesDir: string, cacheDir: string): Promise { - const srcDir = path.join(templatesDir, '/minishift-addon/') - const destDir = path.join(cacheDir, '/templates/minishift-addon/') - await remove(destDir) - await mkdirp(destDir) - await copy(srcDir, destDir) - return destDir - } - -} diff --git a/src/tasks/installers/olm.ts b/src/tasks/installers/olm.ts index d901f0909..0f39e78f2 100644 --- a/src/tasks/installers/olm.ts +++ b/src/tasks/installers/olm.ts @@ -16,8 +16,9 @@ import * as path from 'path' import { KubeHelper } from '../../api/kube' import { CatalogSource, Subscription } from '../../api/typings/olm' -import { CUSTOM_CATALOG_SOURCE_NAME, CVS_PREFIX, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_CHE_OPERATOR_IMAGE, DEFAULT_OLM_KUBERNETES_NAMESPACE, DEFAULT_OPENSHIFT_MARKET_PLACE_NAMESPACE, KUBERNETES_OLM_CATALOG, NIGHTLY_CATALOG_SOURCE_NAME, OLM_NIGHTLY_CHANNEL_NAME, OLM_STABLE_CHANNEL_NAME, OPENSHIFT_OLM_CATALOG, OPERATOR_GROUP_NAME, SUBSCRIPTION_NAME } from '../../constants' -import { isKubernetesPlatformFamily, isStableVersion } from '../../util' +import { VersionHelper } from '../../api/version' +import { CUSTOM_CATALOG_SOURCE_NAME, CVS_PREFIX, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_OLM_KUBERNETES_NAMESPACE, DEFAULT_OPENSHIFT_MARKET_PLACE_NAMESPACE, KUBERNETES_OLM_CATALOG, NIGHTLY_CATALOG_SOURCE_NAME, OLM_NIGHTLY_CHANNEL_NAME, OLM_STABLE_CHANNEL_NAME, OPENSHIFT_OLM_CATALOG, OPERATOR_GROUP_NAME, SUBSCRIPTION_NAME } from '../../constants' +import { isKubernetesPlatformFamily } from '../../util' import { createEclipseCheCluster, createNamespaceTask, patchingEclipseCheCluster } from './common-tasks' @@ -79,20 +80,33 @@ export class OLMTasks { // catalog source name for stable Che version ctx.catalogSourceNameStable = isKubernetesPlatformFamily(flags.platform) ? KUBERNETES_OLM_CATALOG : OPENSHIFT_OLM_CATALOG - if (!flags['auto-update'] && !isStableVersion(flags)) { - ctx.approvalStarategy = 'Automatic' - } else { - ctx.approvalStarategy = flags['auto-update'] ? 'Automatic' : 'Manual' - } - ctx.sourceName = flags['catalog-source-name'] || CUSTOM_CATALOG_SOURCE_NAME ctx.generalPlatformName = isKubernetesPlatformFamily(flags.platform) ? 'kubernetes' : 'openshift' + if (flags.version) { + // Convert version flag to channel (see subscription object), starting CSV and approval starategy + flags.version = VersionHelper.removeVPrefix(flags.version, true) + // Need to point to specific CSV + ctx.startingCSV = `eclipse-che.v${flags.version}` + // Set approval starategy to manual to prevent autoupdate to the latest version right before installation + ctx.approvalStarategy = 'Manual' + } else { + ctx.startingCSV = flags['starting-csv'] + if (ctx.startingCSV) { + // Ignore auto-update flag, otherwise it will automatically update to the latest version and starting-csv will not have any effect. + ctx.approvalStarategy = 'Manual' + } else if (flags['auto-update'] === undefined) { + ctx.approvalStarategy = 'Automatic' + } else { + ctx.approvalStarategy = flags['auto-update'] ? 'Automatic' : 'Manual' + } + } + task.title = `${task.title}...done.` } }, { - enabled: () => !isStableVersion(flags) && !flags['catalog-source-name'] && !flags['catalog-source-yaml'], + enabled: () => !VersionHelper.isDeployingStableVersion(flags) && !flags['catalog-source-name'] && !flags['catalog-source-yaml'], title: `Create nightly index CatalogSource in the namespace ${flags.chenamespace}`, task: async (ctx: any, task: any) => { if (!await kube.catalogSourceExists(NIGHTLY_CATALOG_SOURCE_NAME, flags.chenamespace)) { @@ -128,17 +142,16 @@ export class OLMTasks { task.title = `${task.title}...It already exists.` } else { let subscription: Subscription - - // stable Che CatalogSource - if (isStableVersion(flags)) { - subscription = this.constructSubscription(SUBSCRIPTION_NAME, DEFAULT_CHE_OLM_PACKAGE_NAME, flags.chenamespace, ctx.defaultCatalogSourceNamespace, OLM_STABLE_CHANNEL_NAME, ctx.catalogSourceNameStable, ctx.approvalStarategy, flags['starting-csv']) + if (flags['catalog-source-yaml'] || flags['catalog-source-name']) { // custom Che CatalogSource - } else if (flags['catalog-source-yaml'] || flags['catalog-source-name']) { const catalogSourceNamespace = flags['catalog-source-namespace'] || flags.chenamespace - subscription = this.constructSubscription(SUBSCRIPTION_NAME, flags['package-manifest-name'], flags.chenamespace, catalogSourceNamespace, flags['olm-channel'], ctx.sourceName, ctx.approvalStarategy, flags['starting-csv']) - // nightly Che CatalogSource + subscription = this.constructSubscription(SUBSCRIPTION_NAME, flags['package-manifest-name'], flags.chenamespace, catalogSourceNamespace, flags['olm-channel'], ctx.sourceName, ctx.approvalStarategy, ctx.startingCSV) + } else if (VersionHelper.isDeployingStableVersion(flags)) { + // stable Che CatalogSource + subscription = this.constructSubscription(SUBSCRIPTION_NAME, DEFAULT_CHE_OLM_PACKAGE_NAME, flags.chenamespace, ctx.defaultCatalogSourceNamespace, OLM_STABLE_CHANNEL_NAME, ctx.catalogSourceNameStable, ctx.approvalStarategy, ctx.startingCSV) } else { - subscription = this.constructSubscription(SUBSCRIPTION_NAME, `eclipse-che-preview-${ctx.generalPlatformName}`, flags.chenamespace, flags.chenamespace, OLM_NIGHTLY_CHANNEL_NAME, NIGHTLY_CATALOG_SOURCE_NAME, ctx.approvalStarategy, flags['starting-csv']) + // nightly Che CatalogSource + subscription = this.constructSubscription(SUBSCRIPTION_NAME, `eclipse-che-preview-${ctx.generalPlatformName}`, flags.chenamespace, flags.chenamespace, OLM_NIGHTLY_CHANNEL_NAME, NIGHTLY_CATALOG_SOURCE_NAME, ctx.approvalStarategy, ctx.startingCSV) } await kube.createOperatorSubscription(subscription) task.title = `${task.title}...created new one.` @@ -170,7 +183,7 @@ export class OLMTasks { }, { title: 'Set custom operator image', - enabled: () => flags['che-operator-image'] !== DEFAULT_CHE_OPERATOR_IMAGE, + enabled: () => flags['che-operator-image'], task: async (_ctx: any, task: any) => { const csvList = await kube.getClusterServiceVersions(flags.chenamespace) if (csvList.items.length < 1) { @@ -241,6 +254,14 @@ export class OLMTasks { return } + // Retrieve current and next version from the subscription status + const installedCSV = subscription.status.installedCSV + if (installedCSV) { + ctx.currentVersion = installedCSV.substr(installedCSV.lastIndexOf('v') + 1) + } + const currentCSV = subscription.status.currentCSV + ctx.nextVersion = currentCSV.substr(currentCSV.lastIndexOf('v') + 1) + if (subscription.status.state === 'UpgradePending' && subscription.status!.conditions) { const installCondition = subscription.status.conditions.find(condition => condition.type === 'InstallPlanPending' && condition.status === 'True') if (installCondition) { @@ -249,6 +270,10 @@ export class OLMTasks { return } } + + if (subscription.status.state === 'UpgradeAvailable' && installedCSV === currentCSV) { + command.error('Another update is in progress') + } } command.error('Unable to find installation plan to update.') } @@ -266,6 +291,7 @@ export class OLMTasks { enabled: (ctx: any) => ctx.installPlanName, task: async (ctx: any, task: any) => { await kube.waitUntilOperatorIsInstalled(ctx.installPlanName, flags.chenamespace, 60) + ctx.highlightedMessages.push(`Operator is updated from ${ctx.currentVersion} to ${ ctx.nextVersion} version`) task.title = `${task.title}...done.` } }, diff --git a/src/tasks/installers/operator.ts b/src/tasks/installers/operator.ts index f436e2dd0..ce38e0dc0 100644 --- a/src/tasks/installers/operator.ts +++ b/src/tasks/installers/operator.ts @@ -7,141 +7,173 @@ * * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { V1Deployment } from '@kubernetes/client-node' +import { V1ClusterRole, V1ClusterRoleBinding, V1Deployment, V1Role, V1RoleBinding } from '@kubernetes/client-node' import { Command } from '@oclif/command' import { cli } from 'cli-ux' import * as fs from 'fs' -import * as yaml from 'js-yaml' import * as Listr from 'listr' +import * as path from 'path' +import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' +import { VersionHelper } from '../../api/version' import { CHE_CLUSTER_CRD, CHE_OPERATOR_SELECTOR, OPERATOR_DEPLOYMENT_NAME } from '../../constants' -import { isStableVersion } from '../../util' +import { safeLoadFromYamlFile } from '../../util' import { KubeTasks } from '../kube' -import { copyOperatorResources, createEclipseCheCluster, createNamespaceTask, patchingEclipseCheCluster } from './common-tasks' +import { createEclipseCheCluster, createNamespaceTask, patchingEclipseCheCluster } from './common-tasks' export class OperatorTasks { operatorServiceAccount = 'che-operator' - operatorRole = 'che-operator' - operatorClusterRole = 'che-operator' - operatorRoleBinding = 'che-operator' - operatorClusterRoleBinding = 'che-operator' - namespaceEditorClusterRole = 'che-namespace-editor' - operatorNamespaceEditorClusterRoleBinding = 'che-operator-namespace-editor' cheClusterCrd = 'checlusters.org.eclipse.che' + legacyClusterResourcesName = 'che-operator' - /** - * Returns tasks list which perform preflight platform checks. - */ - startTasks(flags: any, command: Command): Listr { - const clusterRoleName = `${flags.chenamespace}-${this.operatorClusterRole}` - const clusterRoleBindingName = `${flags.chenamespace}-${this.operatorClusterRoleBinding}` - const namespaceEditorClusterRoleName = `${flags.chenamespace}-${this.namespaceEditorClusterRole}` - const operatorNamespaceEditorClusterRoleBindingName = `${flags.chenamespace}-${this.operatorNamespaceEditorClusterRoleBinding}` - const kube = new KubeHelper(flags) - const kubeTasks = new KubeTasks(flags) - if (isStableVersion(flags)) { - command.warn('Consider using the more reliable \'OLM\' installer when deploying a stable release of Eclipse Che (--installer=olm).') - } - return new Listr([ - copyOperatorResources(flags, command.config.cacheDir), - createNamespaceTask(flags.chenamespace, {}), - { - title: `Create ServiceAccount ${this.operatorServiceAccount} in namespace ${flags.chenamespace}`, - task: async (ctx: any, task: any) => { - const exist = await kube.serviceAccountExist(this.operatorServiceAccount, flags.chenamespace) - if (exist) { - task.title = `${task.title}...It already exists.` - } else { - const yamlFilePath = ctx.resourcesPath + 'service_account.yaml' - await kube.createServiceAccountFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...done.` + private getReadRolesAndBindingsTask(kube: KubeHelper): Listr.ListrTask { + return { + title: 'Read Roles and Bindings', + task: async (ctx: any, task: any) => { + ctx.roles = [] + ctx.roleBindings = [] + ctx.clusterRoles = [] + ctx.clusterRoleBindings = [] + const filesList = fs.readdirSync(ctx.resourcesPath) + for (const fileName of filesList) { + if (!fileName.endsWith('.yaml')) { + continue } - } - }, - { - title: `Create Role ${this.operatorRole} in namespace ${flags.chenamespace}`, - task: async (ctx: any, task: any) => { - const exist = await kube.roleExist(this.operatorRole, flags.chenamespace) - if (exist) { - task.title = `${task.title}...It already exists.` - } else { - const yamlFilePath = ctx.resourcesPath + 'role.yaml' - await kube.createRoleFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...done.` + const yamlFilePath = path.join(ctx.resourcesPath, fileName) + const yamlContent = kube.safeLoadFromYamlFile(yamlFilePath) + if (!(yamlContent && yamlContent.kind)) { + continue + } + switch (yamlContent.kind) { + case 'Role': + ctx.roles.push(yamlContent) + break + case 'RoleBinding': + ctx.roleBindings.push(yamlContent) + break + case 'ClusterRole': + ctx.clusterRoles.push(yamlContent) + break + case 'ClusterRoleBinding': + ctx.clusterRoleBindings.push(yamlContent) + break + default: + // Ignore this object kind } } - }, - { - title: `Create ClusterRole ${clusterRoleName}`, - task: async (ctx: any, task: any) => { - const exist = await kube.clusterRoleExist(clusterRoleName) - if (exist) { - task.title = `${task.title}...It already exists.` + + // Check consistancy + if (ctx.roles.length !== ctx.roleBindings.length) { + cli.warn('Number of Roles and Role Bindings is different') + } + if (ctx.clusterRoles.length !== ctx.clusterRoleBindings.length) { + cli.warn('Number of Cluster Roles and Cluster Role Bindings is different') + } + + task.title = `${task.title}...done.` + } + } + } + + private getCreateOrUpdateRolesAndBindingsTask(flags: any, taskTitle: string, shouldUpdate = false): Listr.ListrTask { + const kube = new KubeHelper(flags) + return { + title: taskTitle, + task: async (ctx: any, task: any) => { + if (!ctx.roles) { + // Should never happen. 'Read Roles and Bindings' task should be called first. + throw new Error('Should read Roles and Bindings first') + } + + for (const role of ctx.roles as V1Role[]) { + if (await kube.roleExist(role.metadata!.name, flags.chenamespace)) { + if (shouldUpdate) { + await kube.replaceRoleFrom(role, flags.chenamespace) + } } else { - const yamlFilePath = ctx.resourcesPath + 'cluster_role.yaml' - await kube.createClusterRoleFromFile(yamlFilePath, clusterRoleName) - task.title = `${task.title}...done.` + await kube.createRoleFrom(role, flags.chenamespace) } } - }, - { - title: `Create ClusterRole ${namespaceEditorClusterRoleName}`, - task: async (ctx: any, task: any) => { - ctx.namespaceEditorClusterRoleName = namespaceEditorClusterRoleName - const exist = await kube.clusterRoleExist(namespaceEditorClusterRoleName) - if (exist) { - task.title = `${task.title}...It already exists.` + + for (const roleBinding of ctx.roleBindings as V1RoleBinding[]) { + if (await kube.roleBindingExist(roleBinding.metadata!.name, flags.chenamespace)) { + if (shouldUpdate) { + await kube.replaceRoleBindingFrom(roleBinding, flags.chenamespace) + } } else { - const yamlFilePath = ctx.resourcesPath + 'namespaces_cluster_role.yaml' - await kube.createClusterRoleFromFile(yamlFilePath, namespaceEditorClusterRoleName) - task.title = `${task.title}...done.` + await kube.createRoleBindingFrom(roleBinding, flags.chenamespace) } } - }, - { - title: `Create ClusterRoleBinding ${operatorNamespaceEditorClusterRoleBindingName}`, - task: async (_ctx: any, task: any) => { - const exist = await kube.clusterRoleBindingExist(operatorNamespaceEditorClusterRoleBindingName) - if (exist) { - task.title = `${task.title}...It already exists.` + + // For Cluster Roles and Cluster Role Bindings use prefix to allow several Che installations + const clusterObjectNamePrefix = `${flags.chenamespace}-` + + for (const clusterRole of ctx.clusterRoles as V1ClusterRole[]) { + const clusterRoleName = clusterObjectNamePrefix + clusterRole.metadata!.name + if (await kube.clusterRoleExist(clusterRoleName)) { + if (shouldUpdate) { + await kube.replaceClusterRoleFrom(clusterRole, clusterRoleName) + } } else { - await kube.createClusterRoleBinding(operatorNamespaceEditorClusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, namespaceEditorClusterRoleName) - task.title = `${task.title}...done.` + await kube.createClusterRoleFrom(clusterRole, clusterRoleName) } } - }, - { - title: `Create RoleBinding ${this.operatorRoleBinding} in namespace ${flags.chenamespace}`, - task: async (ctx: any, task: any) => { - const exist = await kube.roleBindingExist(this.operatorRoleBinding, flags.chenamespace) - if (exist) { - task.title = `${task.title}...It already exists.` + + for (const clusterRoleBinding of ctx.clusterRoleBindings as V1ClusterRoleBinding[]) { + clusterRoleBinding.metadata!.name = clusterObjectNamePrefix + clusterRoleBinding.metadata!.name + clusterRoleBinding.roleRef.name = clusterObjectNamePrefix + clusterRoleBinding.roleRef.name + for (const subj of clusterRoleBinding.subjects || []) { + subj.namespace = flags.chenamespace + } + if (await kube.clusterRoleBindingExist(clusterRoleBinding.metadata!.name)) { + if (shouldUpdate) { + await kube.replaceClusterRoleBindingFrom(clusterRoleBinding) + } } else { - const yamlFilePath = ctx.resourcesPath + 'role_binding.yaml' - await kube.createRoleBindingFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...done.` + await kube.createClusterRoleBindingFrom(clusterRoleBinding) } } - }, + + task.title = `${task.title}...done.` + } + } + } + + /** + * Returns tasks list which perform preflight platform checks. + */ + deployTasks(flags: any, command: Command): Listr { + const kube = new KubeHelper(flags) + const kubeTasks = new KubeTasks(flags) + const ctx = ChectlContext.get() + ctx.resourcesPath = path.join(flags.templates, 'che-operator') + if (VersionHelper.isDeployingStableVersion(flags)) { + command.warn('Consider using the more reliable \'OLM\' installer when deploying a stable release of Eclipse Che (--installer=olm).') + } + return new Listr([ + createNamespaceTask(flags.chenamespace, {}), { - title: `Create ClusterRoleBinding ${clusterRoleBindingName}`, - task: async (_ctx: any, task: any) => { - const exist = await kube.clusterRoleBindingExist(clusterRoleBindingName) + title: `Create ServiceAccount ${this.operatorServiceAccount} in namespace ${flags.chenamespace}`, + task: async (ctx: any, task: any) => { + const exist = await kube.serviceAccountExist(this.operatorServiceAccount, flags.chenamespace) if (exist) { task.title = `${task.title}...It already exists.` } else { - await kube.createClusterRoleBinding(clusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, clusterRoleName) + const yamlFilePath = path.join(ctx.resourcesPath, 'service_account.yaml') + await kube.createServiceAccountFromFile(yamlFilePath, flags.chenamespace) task.title = `${task.title}...done.` } } }, + this.getReadRolesAndBindingsTask(kube), + this.getCreateOrUpdateRolesAndBindingsTask(flags, 'Creating Roles and Bindings', false), { title: `Create CRD ${this.cheClusterCrd}`, task: async (ctx: any, task: any) => { const exist = await kube.crdExist(this.cheClusterCrd) - const yamlFilePath = ctx.resourcesPath + 'crds/org_v1_che_crd.yaml' + const yamlFilePath = path.join(ctx.resourcesPath, 'crds', 'org_v1_che_crd.yaml') if (exist) { const checkCRD = await kube.isCRDCompatible(this.cheClusterCrd, yamlFilePath) @@ -169,7 +201,7 @@ export class OperatorTasks { if (exist) { task.title = `${task.title}...It already exists.` } else { - await kube.createDeploymentFromFile(ctx.resourcesPath + 'operator.yaml', flags.chenamespace, flags['che-operator-image']) + await kube.createDeploymentFromFile(path.join(ctx.resourcesPath, 'operator.yaml'), flags.chenamespace, flags['che-operator-image']) task.title = `${task.title}...done.` } } @@ -188,8 +220,8 @@ export class OperatorTasks { } if (!ctx.customCR) { - const yamlFilePath = ctx.resourcesPath + 'crds/org_v1_che_cr.yaml' - ctx.defaultCR = yaml.safeLoad(fs.readFileSync(yamlFilePath).toString()) + const yamlFilePath = path.join(ctx.resourcesPath, 'crds', 'org_v1_che_cr.yaml') + ctx.defaultCR = safeLoadFromYamlFile(yamlFilePath) } task.title = `${task.title}...Done.` @@ -203,37 +235,52 @@ export class OperatorTasks { const kube = new KubeHelper(flags) return new Listr([ { - title: 'Checking versions compatibility before updating', - task: async (ctx: any, _task: any) => { + title: 'Checking existing operator deployment before update', + task: async (ctx: any, task: any) => { const operatorDeployment = await kube.getDeployment(OPERATOR_DEPLOYMENT_NAME, flags.chenamespace) if (!operatorDeployment) { command.error(`${OPERATOR_DEPLOYMENT_NAME} deployment is not found in namespace ${flags.chenamespace}.\nProbably Eclipse Che was initially deployed with another installer`) } - const deployedCheOperator = this.retrieveContainerImage(operatorDeployment) - const deployedCheOperatorImageAndTag = deployedCheOperator.split(':', 2) - ctx.deployedCheOperatorImage = deployedCheOperatorImageAndTag[0] - ctx.deployedCheOperatorTag = deployedCheOperatorImageAndTag.length === 2 ? deployedCheOperatorImageAndTag[1] : 'latest' - - const newCheOperatorImageAndTag = flags['che-operator-image'].split(':', 2) - ctx.newCheOperatorImage = newCheOperatorImageAndTag[0] - ctx.newCheOperatorTag = newCheOperatorImageAndTag.length === 2 ? newCheOperatorImageAndTag[1] : 'latest' + ctx.deployedCheOperatorYaml = operatorDeployment + task.title = `${task.title}...done` + } + }, + { + title: 'Detecting existing version...', + task: async (ctx: any, task: any) => { + ctx.deployedCheOperatorImage = this.retrieveContainerImage(ctx.deployedCheOperatorYaml) + const deployedCheOperatorImageAndTag = ctx.deployedCheOperatorImage.split(':', 2) + ctx.deployedCheOperatorImageName = deployedCheOperatorImageAndTag[0] + ctx.deployedCheOperatorImageTag = deployedCheOperatorImageAndTag.length === 2 ? deployedCheOperatorImageAndTag[1] : 'latest' + ctx.deployedCheOperatorImage = ctx.deployedCheOperatorImageName + ':' + ctx.deployedCheOperatorImageTag + + if (flags['che-operator-image']) { + ctx.newCheOperatorImage = flags['che-operator-image'] + } else { + // Load new operator image from templates + const newCheOperatorYaml = safeLoadFromYamlFile(path.join(flags.templates, 'che-operator', 'operator.yaml')) as V1Deployment + ctx.newCheOperatorImage = this.retrieveContainerImage(newCheOperatorYaml) + } + const newCheOperatorImageAndTag = ctx.newCheOperatorImage.split(':', 2) + ctx.newCheOperatorImageName = newCheOperatorImageAndTag[0] + ctx.newCheOperatorImageTag = newCheOperatorImageAndTag.length === 2 ? newCheOperatorImageAndTag[1] : 'latest' + ctx.newCheOperatorImage = ctx.newCheOperatorImageName + ':' + ctx.newCheOperatorImageTag + + task.title = `${task.title} ${ctx.deployedCheOperatorImageTag} -> ${ctx.newCheOperatorImageTag}` } }]) } updateTasks(flags: any, command: Command): Listr { const kube = new KubeHelper(flags) - const clusterRoleName = `${flags.chenamespace}-${this.operatorClusterRole}` - const clusterRoleBindingName = `${flags.chenamespace}-${this.operatorClusterRoleBinding}` - const namespaceEditorClusterRoleName = `${flags.chenamespace}-${this.namespaceEditorClusterRole}` - const operatorNamespaceEditorClusterRoleBindingName = `${flags.chenamespace}-${this.operatorNamespaceEditorClusterRoleBinding}` + const ctx = ChectlContext.get() + ctx.resourcesPath = path.join(flags.templates, 'che-operator') return new Listr([ - copyOperatorResources(flags, command.config.cacheDir), { title: `Updating ServiceAccount ${this.operatorServiceAccount} in namespace ${flags.chenamespace}`, task: async (ctx: any, task: any) => { const exist = await kube.serviceAccountExist(this.operatorServiceAccount, flags.chenamespace) - const yamlFilePath = ctx.resourcesPath + 'service_account.yaml' + const yamlFilePath = path.join(ctx.resourcesPath, 'service_account.yaml') if (exist) { await kube.replaceServiceAccountFromFile(yamlFilePath, flags.chenamespace) task.title = `${task.title}...updated.` @@ -243,103 +290,13 @@ export class OperatorTasks { } } }, - { - title: `Updating Role ${this.operatorRole} in namespace ${flags.chenamespace}`, - task: async (ctx: any, task: any) => { - const exist = await kube.roleExist(this.operatorRole, flags.chenamespace) - const yamlFilePath = ctx.resourcesPath + 'role.yaml' - if (exist) { - await kube.replaceRoleFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...updated.` - } else { - await kube.createRoleFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...created new one.` - } - } - }, - { - title: `Updating ClusterRole ${clusterRoleName}`, - task: async (ctx: any, task: any) => { - const clusterRoleExists = await kube.clusterRoleExist(clusterRoleName) - const legacyClusterRoleExists = await kube.clusterRoleExist(this.operatorClusterRole) - const yamlFilePath = ctx.resourcesPath + 'cluster_role.yaml' - if (clusterRoleExists) { - await kube.replaceClusterRoleFromFile(yamlFilePath, clusterRoleName) - task.title = `${task.title}...updated.` - // it is needed to check the legacy cluster object name to be compatible with previous installations - } else if (legacyClusterRoleExists) { - await kube.replaceClusterRoleFromFile(yamlFilePath, this.operatorClusterRole) - task.title = `Updating ClusterRole ${this.operatorClusterRole}...updated.` - } else { - await kube.createClusterRoleFromFile(yamlFilePath, clusterRoleName) - task.title = `${task.title}...created a new one.` - } - } - }, - { - title: `Updating ClusterRole ${namespaceEditorClusterRoleName}`, - task: async (ctx: any, task: any) => { - const clusterRoleExists = await kube.clusterRoleExist(namespaceEditorClusterRoleName) - const yamlFilePath = ctx.resourcesPath + 'namespaces_cluster_role.yaml' - if (clusterRoleExists) { - await kube.replaceClusterRoleFromFile(yamlFilePath, namespaceEditorClusterRoleName) - task.title = `${task.title}...updated.` - } else { - await kube.createClusterRoleFromFile(yamlFilePath, namespaceEditorClusterRoleName) - task.title = `${task.title}...created a new one.` - } - } - }, - { - title: `Updating RoleBinding ${this.operatorRoleBinding} in namespace ${flags.chenamespace}`, - task: async (ctx: any, task: any) => { - const exist = await kube.roleBindingExist(this.operatorRoleBinding, flags.chenamespace) - const yamlFilePath = ctx.resourcesPath + 'role_binding.yaml' - if (exist) { - await kube.replaceRoleBindingFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...updated.` - } else { - await kube.createRoleBindingFromFile(yamlFilePath, flags.chenamespace) - task.title = `${task.title}...created new one.` - } - } - }, - { - title: `Updating ClusterRoleBinding ${clusterRoleBindingName}`, - task: async (_ctx: any, task: any) => { - const clusterRoleBindExists = await kube.clusterRoleBindingExist(clusterRoleBindingName) - const legacyClusterRoleBindExists = await kube.clusterRoleBindingExist(this.operatorClusterRoleBinding) - if (clusterRoleBindExists) { - await kube.replaceClusterRoleBinding(clusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, clusterRoleName) - task.title = `${task.title}...updated.` - // it is needed to check the legacy cluster object name to be compatible with previous installations - } else if (legacyClusterRoleBindExists) { - await kube.replaceClusterRoleBinding(this.operatorClusterRoleBinding, this.operatorServiceAccount, flags.chenamespace, this.operatorClusterRole) - task.title = `Updating ClusterRoleBinding ${this.operatorClusterRoleBinding}...updated.` - } else { - await kube.createClusterRoleBinding(clusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, clusterRoleName) - task.title = `${task.title}...created new one.` - } - } - }, - { - title: `Updating ClusterRoleBinding ${operatorNamespaceEditorClusterRoleBindingName}`, - task: async (_ctx: any, task: any) => { - const clusterRoleBindExists = await kube.clusterRoleBindingExist(operatorNamespaceEditorClusterRoleBindingName) - if (clusterRoleBindExists) { - await kube.replaceClusterRoleBinding(operatorNamespaceEditorClusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, namespaceEditorClusterRoleName) - task.title = `${task.title}...updated.` - } else { - await kube.createClusterRoleBinding(operatorNamespaceEditorClusterRoleBindingName, this.operatorServiceAccount, flags.chenamespace, namespaceEditorClusterRoleName) - task.title = `${task.title}...created new one.` - } - } - }, + this.getReadRolesAndBindingsTask(kube), + this.getCreateOrUpdateRolesAndBindingsTask(flags, 'Updating Roles and Bindings', true), { title: `Updating Eclipse Che cluster CRD ${this.cheClusterCrd}`, task: async (ctx: any, task: any) => { const crd = await kube.getCrd(this.cheClusterCrd) - const yamlFilePath = ctx.resourcesPath + 'crds/org_v1_che_crd.yaml' + const yamlFilePath = path.join(ctx.resourcesPath, 'crds', 'org_v1_che_crd.yaml') if (crd) { if (!crd.metadata || !crd.metadata.resourceVersion) { throw new Error(`Fetched CRD ${this.cheClusterCrd} without resource version`) @@ -364,11 +321,12 @@ export class OperatorTasks { title: `Updating deployment ${OPERATOR_DEPLOYMENT_NAME} in namespace ${flags.chenamespace}`, task: async (ctx: any, task: any) => { const exist = await kube.deploymentExist(OPERATOR_DEPLOYMENT_NAME, flags.chenamespace) + const deploymentPath = path.join(ctx.resourcesPath, 'operator.yaml') if (exist) { - await kube.replaceDeploymentFromFile(ctx.resourcesPath + 'operator.yaml', flags.chenamespace, flags['che-operator-image']) + await kube.replaceDeploymentFromFile(deploymentPath, flags.chenamespace, flags['che-operator-image']) task.title = `${task.title}...updated.` } else { - await kube.createDeploymentFromFile(ctx.resourcesPath + 'operator.yaml', flags.chenamespace, flags['che-operator-image']) + await kube.createDeploymentFromFile(deploymentPath, flags.chenamespace, flags['che-operator-image']) task.title = `${task.title}...created new one.` } } @@ -389,10 +347,6 @@ export class OperatorTasks { */ deleteTasks(flags: any): ReadonlyArray { let kh = new KubeHelper(flags) - const clusterRoleName = `${flags.chenamespace}-${this.operatorClusterRole}` - const clusterRoleBindingName = `${flags.chenamespace}-${this.operatorClusterRoleBinding}` - const namespaceEditorClusterRoleName = `${flags.chenamespace}-${this.namespaceEditorClusterRole}` - const operatorNamespaceEditorClusterRoleBindingName = `${flags.chenamespace}-${this.operatorNamespaceEditorClusterRoleBinding}` return [{ title: 'Delete oauthClientAuthorizations', task: async (_ctx: any, task: any) => { @@ -416,7 +370,7 @@ export class OperatorTasks { do { await cli.wait(2000) //wait a couple of secs for the finalizers to be executed } while (await kh.getCheCluster(flags.chenamespace)) - task.title = await `${task.title}...OK` + task.title = `${task.title}...OK` } }, { @@ -425,97 +379,60 @@ export class OperatorTasks { const crdExists = await kh.crdExist(this.cheClusterCrd) const checlusters = await kh.getAllCheClusters() if (checlusters.length > 0) { - task.title = await `${task.title}...Skipped: another Eclipse Che deployment found.` + task.title = `${task.title}...Skipped: another Eclipse Che deployment found.` } else { // Check if CRD exist. When installer is helm the CRD are not created if (crdExists) { await kh.deleteCrd(this.cheClusterCrd) } - task.title = await `${task.title}...OK` - } - } - }, - { - title: `Delete role binding ${this.operatorRoleBinding}`, - task: async (_ctx: any, task: any) => { - if (await kh.roleBindingExist(this.operatorRoleBinding, flags.chenamespace)) { - await kh.deleteRoleBinding(this.operatorRoleBinding, flags.chenamespace) - } - task.title = await `${task.title}...OK` - } - }, - { - title: `Delete role ${this.operatorRole}`, - task: async (_ctx: any, task: any) => { - if (await kh.roleExist(this.operatorRole, flags.chenamespace)) { - await kh.deleteRole(this.operatorRole, flags.chenamespace) - } - task.title = await `${task.title}...OK` - } - }, - { - title: `Delete cluster role binding ${clusterRoleBindingName}`, - task: async (_ctx: any, task: any) => { - const clusterRoleBindExists = await kh.clusterRoleBindingExist(clusterRoleBindingName) - const legacyClusterRoleBindExists = await kh.clusterRoleBindingExist(this.operatorClusterRoleBinding) - if (clusterRoleBindExists) { - await kh.deleteClusterRoleBinding(clusterRoleBindingName) - task.title = await `${task.title}...OK` - // it is needed to check the legacy cluster object name to be compatible with previous installations - } else if (legacyClusterRoleBindExists) { - await kh.deleteClusterRoleBinding(this.operatorClusterRoleBinding) - task.title = await `Delete cluster role binding ${this.operatorClusterRoleBinding}...OK` - } - } - }, - { - title: `Delete cluster role ${clusterRoleName}`, - task: async (_ctx: any, task: any) => { - const clusterRoleExists = await kh.clusterRoleExist(clusterRoleName) - const legacyClusterRoleExists = await kh.clusterRoleExist(this.operatorClusterRole) - if (clusterRoleExists) { - await kh.deleteClusterRole(clusterRoleName) - task.title = await `${task.title}...OK` - // it is needed to check the legacy cluster object name to be compatible with previous installations - } else if (legacyClusterRoleExists) { - await kh.deleteClusterRole(this.operatorClusterRole) - task.title = await `Delete cluster role ${this.operatorClusterRole}...OK` + task.title = `${task.title}...OK` } } }, { - title: `Delete cluster role binding ${operatorNamespaceEditorClusterRoleBindingName}`, + title: 'Delete Roles and Bindings', task: async (_ctx: any, task: any) => { - const clusterRoleBindExists = await kh.clusterRoleBindingExist(operatorNamespaceEditorClusterRoleBindingName) - if (clusterRoleBindExists) { - await kh.deleteClusterRoleBinding(operatorNamespaceEditorClusterRoleBindingName) - task.title = await `${task.title}...OK` + const roleBindings = await kh.listRoleBindings(flags.chenamespace) + for (const roleBinding of roleBindings.items) { + await kh.deleteRoleBinding(roleBinding.metadata!.name, flags.chenamespace) } - } - }, - { - title: `Delete cluster role ${namespaceEditorClusterRoleName}`, - task: async (_ctx: any, task: any) => { - const clusterRoleExists = await kh.clusterRoleExist(namespaceEditorClusterRoleName) - if (clusterRoleExists) { - await kh.deleteClusterRole(namespaceEditorClusterRoleName) - task.title = await `${task.title}...OK` + + const roles = await kh.listRoles(flags.chenamespace) + for (const role of roles.items) { + await kh.deleteRole(role.metadata!.name, flags.chenamespace) } - } - }, - { - title: 'Delete server and workspace rolebindings', - task: async (_ctx: any, task: any) => { - if (await kh.roleBindingExist('che', flags.chenamespace)) { - await kh.deleteRoleBinding('che', flags.chenamespace) + + // Count existing pairs of cluster roles and thier bindings + let pairs = 0 + + const clusterRoleBindings = await kh.listClusterRoleBindings() + for (const clusterRoleBinding of clusterRoleBindings.items) { + const name = clusterRoleBinding.metadata && clusterRoleBinding.metadata.name || '' + if (name.startsWith(flags.chenamespace)) { + pairs++ + await kh.deleteClusterRoleBinding(name) + } } - if (await kh.roleBindingExist('che-workspace-exec', flags.chenamespace)) { - await kh.deleteRoleBinding('che-workspace-exec', flags.chenamespace) + + const clusterRoles = await kh.listClusterRoles() + for (const clusterRole of clusterRoles.items) { + const name = clusterRole.metadata && clusterRole.metadata.name || '' + if (name.startsWith(flags.chenamespace)) { + await kh.deleteClusterRole(name) + } } - if (await kh.roleBindingExist('che-workspace-view', flags.chenamespace)) { - await kh.deleteRoleBinding('che-workspace-view', flags.chenamespace) + + // If no pairs were deleted, then legacy names is used + if (pairs === 0) { + if (await kh.clusterRoleBindingExist(this.legacyClusterResourcesName)) { + await kh.deleteClusterRoleBinding(this.legacyClusterResourcesName) + } + if (await kh.clusterRoleExist(this.legacyClusterResourcesName)) { + await kh.deleteClusterRole(this.legacyClusterResourcesName) + } } - task.title = await `${task.title}...OK` + + task.title = `${task.title}...OK` } }, { @@ -524,7 +441,7 @@ export class OperatorTasks { if (await kh.serviceAccountExist(this.operatorServiceAccount, flags.chenamespace)) { await kh.deleteServiceAccount(this.operatorServiceAccount, flags.chenamespace) } - task.title = await `${task.title}...OK` + task.title = `${task.title}...OK` } }, { @@ -533,23 +450,12 @@ export class OperatorTasks { if (await kh.persistentVolumeClaimExist('che-operator', flags.chenamespace)) { await kh.deletePersistentVolumeClaim('che-operator', flags.chenamespace) } - task.title = await `${task.title}...OK` + task.title = `${task.title}...OK` } }, ] } - async evaluateTemplateOperatorImage(flags: any): Promise { - if (flags['che-operator-image']) { - return flags['che-operator-image'] - } else { - const filePath = flags.templates + '/che-operator/operator.yaml' - const yamlFile = fs.readFileSync(filePath) - const yamlDeployment = yaml.safeLoad(yamlFile.toString()) as V1Deployment - return yamlDeployment.spec!.template.spec!.containers[0].image! - } - } - retrieveContainerImage(deployment: V1Deployment) { const containers = deployment.spec!.template!.spec!.containers diff --git a/src/util.ts b/src/util.ts index 98f448a43..4e478ebe6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,17 +9,20 @@ **********************************************************************/ import axios from 'axios' +import { cli } from 'cli-ux' import * as commandExists from 'command-exists' import * as fs from 'fs-extra' import * as https from 'https' import * as yaml from 'js-yaml' import * as notifier from 'node-notifier' +import * as path from 'path' const pkjson = require('../package.json') import { ChectlContext } from './api/context' import { KubeHelper } from './api/kube' -import { DEFAULT_CHE_NAMESPACE, DEFAULT_CHE_OPERATOR_IMAGE, LEGACY_CHE_NAMESPACE } from './constants' +import { VersionHelper } from './api/version' +import { DEFAULT_CHE_NAMESPACE, LEGACY_CHE_NAMESPACE } from './constants' export const KUBERNETES_CLI = 'kubectl' export const OPENSHIFT_CLI = 'oc' @@ -76,15 +79,6 @@ export function base64Decode(arg: string): string { return Buffer.from(arg, 'base64').toString('ascii') } -/** - * Indicates if stable version of `chectl` is used. - */ -export function isStableVersion(flags: any): boolean { - const operatorImage = flags['che-operator-image'] || DEFAULT_CHE_OPERATOR_IMAGE - const cheVersion = getImageTag(operatorImage) - return cheVersion !== 'nightly' && cheVersion !== 'latest' && !flags['catalog-source-yaml'] && !flags['catalog-source-name'] -} - /** * Returns the tag of the image. */ @@ -118,7 +112,7 @@ export function readCRFile(flags: any, CRKey: string): any { } if (fs.existsSync(CRFilePath)) { - return yaml.safeLoad(fs.readFileSync(CRFilePath).toString()) + return safeLoadFromYamlFile(CRFilePath) } throw new Error(`Unable to find file defined in the flag '--${CRKey}'`) @@ -146,6 +140,21 @@ export function getCommandSuccessMessage(): string { return `Command ${ctx[ChectlContext.COMMAND_ID]} has completed successfully.` } +/** + * Returns command error message. + */ +export function getCommandErrorMessage(err: Error): string { + const ctx = ChectlContext.get() + const logDirectory = ctx[ChectlContext.LOGS_DIR] + + let message = `${err}\nCommand ${ctx[ChectlContext.COMMAND_ID]} failed. Error log: ${ctx[ChectlContext.ERROR_LOG]}` + if (logDirectory && isDirEmpty(logDirectory)) { + message += ` Eclipse Che logs: ${logDirectory}` + } + + return message +} + export function notifyCommandCompletedSuccessfully(): void { notifier.notify({ title: 'chectl', @@ -153,6 +162,17 @@ export function notifyCommandCompletedSuccessfully(): void { }) } +export async function askForChectlUpdateIfNeeded(): Promise { + const ctx = ChectlContext.get() + if (await VersionHelper.isChectlUpdateAvailable(ctx[ChectlContext.CACHE_DIR])) { + cli.info('A newer version of chectl is available.') + if (await cli.confirm('To deploy the latest version of Eclipse Che you have to update chectl first [y/n]')) { + cli.info('Please run "chectl update" and then repeat "server:deploy" command.') + cli.exit(0) + } + } +} + /** * Determine if a directory is empty. */ @@ -165,21 +185,6 @@ export function isDirEmpty(dirname: string): boolean { } } -/** - * Returns command success message with execution time. - */ -export function getCommandErrorMessage(err: Error): string { - const ctx = ChectlContext.get() - const logDirectory = ctx[ChectlContext.LOGS_DIRECTORY] - - let message = `${err}\nCommand ${ctx[ChectlContext.COMMAND_ID]} failed. Error log: ${ctx[ChectlContext.ERROR_LOG]}` - if (logDirectory && isDirEmpty(logDirectory)) { - message += ` Eclipse Che logs: ${logDirectory}` - } - - return message -} - /** * Returns current chectl version defined in package.json. */ @@ -198,24 +203,43 @@ export function readPackageJson(): any { return JSON.parse(fs.readFileSync('../package.json').toString()) } +export function safeLoadFromYamlFile(filePath: string): any { + return yaml.safeLoad(fs.readFileSync(filePath).toString()) +} + +export function safeSaveYamlToFile(yamlObject: any, filePath: string): void { + fs.writeFileSync(filePath, yaml.safeDump(yamlObject)) +} + +export async function downloadFile(url: string, dest: string): Promise { + const streamWriter = fs.createWriteStream(dest) + const response = await axios({ url, method: 'GET', responseType: 'stream' }) + response.data.pipe(streamWriter) + return new Promise((resolve, reject) => { + streamWriter.on('finish', resolve) + streamWriter.on('error', reject) + }) +} + /** - * Returns latest chectl version for the given channel. + * Downloads yaml file and returns data converted to JSON. + * @param url link to yaml file */ -export async function getLatestChectlVersion(channel: string): Promise { - if (getProjectName() !== 'chectl') { - return - } - +export async function downloadYaml(url: string): Promise { const axiosInstance = axios.create({ httpsAgent: new https.Agent({}) }) + const response = await axiosInstance.get(url) + return yaml.safeLoad(response.data) +} - try { - const { data } = await axiosInstance.get(`https://che-incubator.github.io/chectl/channels/${channel}/linux-x64`) - return data.version - } catch { - return +export function getEmbeddedTemplatesDirectory(): string { + if (__dirname.endsWith('src')) { + // Development version + return path.join(__dirname, '..', 'templates') } + // Release (including nightly) version + return path.join(__dirname, '..', '..', '..', 'templates') } /** diff --git a/test/api/version.test.ts b/test/api/version.test.ts index efb3f69cd..8433787c7 100644 --- a/test/api/version.test.ts +++ b/test/api/version.test.ts @@ -11,30 +11,32 @@ import { expect, fancy } from 'fancy-test' import { VersionHelper } from '../../src/api/version' -describe('OpenShift API helper', () => { - fancy - .it('check minimal version: case #1', async () => { - const check = VersionHelper.checkMinimalVersion('v2.10', 'v2.10') - expect(check).to.true - }) - fancy - .it('check minimal version: case #2', async () => { - const check = VersionHelper.checkMinimalVersion('v3.12', 'v2.10') - expect(check).to.true - }) - fancy - .it('check minimal version: case #3', async () => { - const check = VersionHelper.checkMinimalVersion('v2.11', 'v2.10') - expect(check).to.true - }) - fancy - .it('check minimal version: case #4', async () => { - const check = VersionHelper.checkMinimalVersion('v2.09', 'v2.10') - expect(check).to.false - }) - fancy - .it('check minimal version: case #5', async () => { - const check = VersionHelper.checkMinimalVersion('v2.10', 'v3.10') - expect(check).to.false - }) +describe('Version Helper', () => { + describe('OpenShift API helper', () => { + fancy + .it('check minimal version: case #1', async () => { + const check = VersionHelper.checkMinimalVersion('v2.10', 'v2.10') + expect(check).to.true + }) + fancy + .it('check minimal version: case #2', async () => { + const check = VersionHelper.checkMinimalVersion('v3.12', 'v2.10') + expect(check).to.true + }) + fancy + .it('check minimal version: case #3', async () => { + const check = VersionHelper.checkMinimalVersion('v2.11', 'v2.10') + expect(check).to.true + }) + fancy + .it('check minimal version: case #4', async () => { + const check = VersionHelper.checkMinimalVersion('v2.09', 'v2.10') + expect(check).to.false + }) + fancy + .it('check minimal version: case #5', async () => { + const check = VersionHelper.checkMinimalVersion('v2.10', 'v3.10') + expect(check).to.false + }) + }) }) diff --git a/test/tasks/installers/minishift-addon.test.ts b/test/tasks/installers/minishift-addon.test.ts deleted file mode 100644 index 282d1f96f..000000000 --- a/test/tasks/installers/minishift-addon.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/********************************************************************* - * Copyright (c) 2019 Red Hat, Inc. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - **********************************************************************/ -// tslint:disable:object-curly-spacing -import { expect, fancy } from 'fancy-test' -import * as execa from 'execa' - -import { MinishiftAddonTasks } from '../../../src/tasks/installers/minishift-addon' - -jest.mock('execa') - -let minishiftAddonTasks = new MinishiftAddonTasks() -describe('Minishift addon helper', () => { - fancy - .it('extracts the tag part from an image name', async () => { - const image = 'quay.io/eclipse/che:latest' - const tag = minishiftAddonTasks.getImageTag(image) - expect(tag).to.equal('latest') - }) - - fancy - .it('extracts the repo part from an image name', async () => { - const image = 'quay.io/eclipse/che:latest' - const repository = minishiftAddonTasks.getImageRepository(image) - expect(repository).to.equal('quay.io/eclipse/che') - }) - - fancy - .it('returns the repo part even if an image has no tag', async () => { - const image = 'quay.io/eclipse/che' - const repository = minishiftAddonTasks.getImageRepository(image) - expect(repository).to.equal('quay.io/eclipse/che') - }) - - fancy - .it('returns latest as tag if an image has no tag', async () => { - const image = 'quay.io/eclipse/che' - const tag = minishiftAddonTasks.getImageTag(image) - expect(tag).to.equal('latest') - }) - - fancy - .it('check grab Version 1.34', async () => { - const minishiftVersionOutput = 'minishift v1.34.0+f5db7cb'; - (execa as any).mockResolvedValue({ exitCode: 0, stdout: minishiftVersionOutput }) - const version = await minishiftAddonTasks.grabVersion(); - expect(version).to.equal(134) - }) - - fancy - .it('check grab Version 1.33', async () => { - const minishiftVersionOutput = 'minishift v1.33.0+ba29431'; - (execa as any).mockResolvedValue({ exitCode: 0, stdout: minishiftVersionOutput }) - const version = await minishiftAddonTasks.grabVersion(); - expect(version).to.equal(133) - }) -}) diff --git a/yarn.lock b/yarn.lock index e3bf9244c..7af4b9cf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -701,6 +701,109 @@ tslint-eslint-rules "^5.4.0" tslint-xo "^0.9.0" +"@octokit/auth-token@^2.4.4": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.2.3": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.5.tgz#57becbd5fd789b0592b915840855f3a5f233d554" + integrity sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.4.12" + "@octokit/types" "^6.0.3" + before-after-hook "^2.1.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.11" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1" + integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.5.9" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.9.tgz#2365831a1a88f4cb6fd4b0488edb6587d8243024" + integrity sha512-c+0yofIugUNqo+ktrLaBlWSbjSq/UF8ChAyxQzbD3X74k1vAuyLKdDJmPwVExUFSp6+U1FzWe+3OkeRsIqV0vg== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-3.4.1.tgz#317f78cede7f387046e6bad2041e01ddf9607e96" + integrity sha512-7Sjm3UwEAM11f+ck9+qlyEfgl8hCk5sSZBU2qcWY8+8ibowjqcwxhhtvY0/pjHPF8mcvmedFpGmmIYs2qM9/+Q== + +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.9.0.tgz#f52c26850b019584be8dc55c0bd6257339c0fa43" + integrity sha512-XxbOg45r2n/2QpU6hnGDxQNDRrJ7gjYpMXeDbUCigWTHECmjoyFLizkFO2jMEtidMkfiELn7AF8GBAJ/cbPTnA== + dependencies: + "@octokit/types" "^6.6.0" + +"@octokit/plugin-request-log@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d" + integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ== + +"@octokit/plugin-rest-endpoint-methods@4.8.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.8.0.tgz#c1f24f940fc265f0021c8f544e3d8755f3253759" + integrity sha512-2zRpXDveJH8HsXkeeMtRW21do8wuSxVn1xXFdvhILyxlLWqGQrdJUA1/dk5DM7iAAYvwT/P3bDOLs90yL4S2AA== + dependencies: + "@octokit/types" "^6.5.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143" + integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.3.0", "@octokit/request@^5.4.12": + version "5.4.14" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.14.tgz#ec5f96f78333bb2af390afa5ff66f114b063bc96" + integrity sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^6.7.1" + deprecation "^2.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + once "^1.4.0" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.0.12": + version "18.0.15" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.15.tgz#a690aabd9927a3389e285dee25da67e62b3f14ad" + integrity sha512-MBlZl0KeuvFMJ3210hG5xhh/jtYmMDLd5WmO49Wg4Rfg0odeivntWAyq3KofJDP2G8jskCaaOaZBKo0TeO9tFA== + dependencies: + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.8.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.5.0", "@octokit/types@^6.6.0", "@octokit/types@^6.7.1": + version "6.7.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.7.1.tgz#01fdc5bbc67bf10ab0ca0aa2461d19ff9dd67fb7" + integrity sha512-OzRXbizUfixgzTjlSZQj+yuo0J9vAMOLtpsIm3JjQUsI3CcLXZnVaxRIWtYD+iwHznnvG9fJlPHM6SRp77fUcw== + dependencies: + "@octokit/openapi-types" "^3.4.1" + "@types/node" ">= 8" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -795,7 +898,7 @@ dependencies: "@types/node" "*" -"@types/glob@^7.1.1": +"@types/glob@*", "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== @@ -902,6 +1005,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.1.tgz#56af902ad157e763f9ba63d671c39cda3193c835" integrity sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw== +"@types/node@>= 8": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== + "@types/node@^10.12.0": version "10.17.35" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" @@ -932,11 +1040,24 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/rimraf@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.0.tgz#b9d03f090ece263671898d57bb7bb007023ac19f" + integrity sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/semver@^7.3.4": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + "@types/sinon@*": version "9.0.5" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.5.tgz#56b2a12662dd8c7d081cdc511af5f872cb37377f" @@ -979,6 +1100,13 @@ resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.23.tgz#cc672e8864000d288e1e39c609fd9cab84391ff3" integrity sha512-vX1NPekXhrLquFWskH2thcvFAha187F/lM6xYOoEMZWwJ/6alSk0/ttmGP/YRqcqtCv0TMbZjYAdZyHAEcuU4g== +"@types/unzipper@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@types/unzipper/-/unzipper-0.10.3.tgz#9eea872fb1fa460da76f253878b6275af588f464" + integrity sha512-01mQdTLp3/KuBVDhP82FNBf+enzVOjJ9dGsCWa5z8fcYAFVgA9bqIQ2NmsgNFzN/DhD0PUQj4n5p7k6I9mq80g== + dependencies: + "@types/node" "*" + "@types/websocket@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.1.tgz#039272c196c2c0e4868a0d8a1a27bbb86e9e9138" @@ -1376,11 +1504,29 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +before-after-hook@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.1.tgz#99ae36992b5cfab4a83f6bee74ab27835f28f405" + integrity sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA== + +big-integer@^1.6.17: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1405,6 +1551,11 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1494,6 +1645,11 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -1502,6 +1658,11 @@ buffer@^5.5.0: base64-js "^1.0.2" ieee754 "^1.1.4" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1587,6 +1748,13 @@ chai@^4.2.0: pathval "^1.1.0" type-detect "^4.0.5" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= + dependencies: + traverse ">=0.3.0 <0.4" + chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2099,6 +2267,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + detect-indent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" @@ -2146,6 +2319,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -2170,7 +2350,7 @@ ecc-jsbn@~0.1.1: "eclipse-che-devfile-workspace-operator@git://github.com/devfile/devworkspace-operator#master": version "0.0.0" - resolved "git://github.com/devfile/devworkspace-operator#0c5d588f71dc7863617c217a79771bda8b3c3ef0" + resolved "git://github.com/devfile/devworkspace-operator#3bdccac9f3aa859d083c75a528b3d2d4556d6fdb" "eclipse-che-minishift@git://github.com/minishift/minishift#master": version "0.0.0" @@ -2182,7 +2362,7 @@ ecc-jsbn@~0.1.1: "eclipse-che@git://github.com/eclipse/che#master": version "0.0.0" - resolved "git://github.com/eclipse/che#4245b646c9b2298f232a5e875529b4f0aca9997d" + resolved "git://github.com/eclipse/che#cf3c4dc3e4d23f33adafb6ef497d1bcc3c4db41e" editorconfig@^0.15.0: version "0.15.3" @@ -2692,6 +2872,16 @@ fsevents@^2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -2850,7 +3040,7 @@ got@^8.3.2: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -3039,7 +3229,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3284,6 +3474,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -3994,6 +4189,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= + listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -4158,6 +4358,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4331,7 +4538,7 @@ mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -4416,6 +4623,11 @@ nock@^11.7.0: mkdirp "^0.5.0" propagate "^2.0.0" +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -4977,7 +5189,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.3.0, readable-stream@^2.3.5: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -5181,14 +5393,14 @@ rfc4648@^1.3.0: resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== -rimraf@^2.6.3: +rimraf@2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -5276,6 +5488,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -5291,6 +5510,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -5872,6 +6096,11 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= + treeify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" @@ -6068,6 +6297,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -6086,6 +6320,22 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unzipper@0.10.11: + version "0.10.11" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" + integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602"