From dbe54080c53ec461e8271c15f01546144d4a8612 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Thu, 13 Jan 2022 11:32:25 +0200 Subject: [PATCH] feat: Install next dev-workspace operator for all-namespaces next channel (#1913) * feat: Use custom catalogs to deploy Dev Workspace operator Signed-off-by: Oleksandr Andriienko Signed-off-by: Anatolii Bazko Co-authored-by: Anatolii Bazko --- src/api/context.ts | 6 + src/api/kube.ts | 101 ++++++++- src/api/types/olm.ts | 8 + src/commands/server/delete.ts | 29 ++- src/commands/server/deploy.ts | 2 +- src/commands/server/restore.ts | 8 +- src/constants.ts | 17 +- .../devfile-workspace-operator-installer.ts | 179 +++++++++------ src/tasks/installers/installer.ts | 6 +- .../installers/olm-dev-workspace-operator.ts | 205 ++++++++++++++++++ src/tasks/installers/olm.ts | 167 ++++++++------ src/tasks/installers/operator.ts | 2 +- test/e2e/e2e.test.ts | 2 +- 13 files changed, 569 insertions(+), 163 deletions(-) create mode 100644 src/tasks/installers/olm-dev-workspace-operator.ts diff --git a/src/api/context.ts b/src/api/context.ts index 44fcb66da..1d8e755b8 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -97,6 +97,12 @@ export namespace OLM { export const PACKAGE_MANIFEST_NAME = 'package-manifest-name' } +export namespace DevWorkspaceContextKeys { + export const IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB = 'is-dev-workspace-installed-via-operator-hub' + export const CATALOG_SOURCE_NAME = 'dev-workspace-catalog-source-name' + export const INSTALL_PLAN = 'dev-workspace-install-plan' +} + export enum OLMInstallationUpdate { MANUAL = 'Manual', AUTO = 'Automatic' diff --git a/src/api/kube.ts b/src/api/kube.ts index bfc6bd2b2..b0a27a241 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -20,7 +20,7 @@ import * as https from 'https' import { merge } from 'lodash' import * as net from 'net' import { Writable } from 'stream' -import { CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_BACKUP_KIND_PLURAL, CHE_CLUSTER_KIND_PLURAL, CHE_CLUSTER_RESTORE_KIND_PLURAL, DEFAULT_CHE_TLS_SECRET_NAME, DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT, DEFAULT_K8S_POD_WAIT_TIMEOUT, OLM_STABLE_CHANNEL_NAME } from '../constants' +import { CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_BACKUP_KIND_PLURAL, CHE_CLUSTER_KIND_PLURAL, CHE_CLUSTER_RESTORE_KIND_PLURAL, DEFAULT_CHE_TLS_SECRET_NAME, DEFAULT_K8S_POD_ERROR_RECHECK_TIMEOUT, DEFAULT_K8S_POD_WAIT_TIMEOUT, DEVFILE_WORKSPACE_API_GROUP, DEVFILE_WORKSPACE_API_VERSION, DEVFILE_WORKSPACE_KIND_PLURAL, OLM_STABLE_CHANNEL_NAME } from '../constants' import { base64Encode, getClusterClientCommand, getImageNameAndTag, isKubernetesPlatformFamily, newError, safeLoadFromYamlFile } from '../util' import { ChectlContext, OLM } from './context' import { V1CheClusterBackup, V1CheClusterRestore } from './types/backup-restore-crds' @@ -93,6 +93,18 @@ export class KubeHelper { throw new Error(`Namespace '${name}' is not in 'Active' phase.`) } + async deleteService(name: string, namespace: string): Promise { + const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) + + try { + await k8sApi.deleteNamespacedService(name, namespace) + } catch (e) { + if (e.response.statusCode !== 404) { + throw this.wrapK8sClientError(e) + } + } + } + async deleteAllServices(namespace: string): Promise { const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) try { @@ -102,7 +114,7 @@ export class KubeHelper { await serviceList.items.forEach(async service => { try { await k8sApi.deleteNamespacedService(service.metadata!.name!, namespace) - } catch (error) { + } catch (error: any) { if (error.response.statusCode !== 404) { throw error } @@ -1268,12 +1280,12 @@ export class KubeHelper { async deleteDeployment(namespace: string, name: string): Promise { const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) try { - k8sAppsApi.deleteNamespacedDeployment(name, namespace) - } catch (error) { - if (error.response && error.response.statusCode === 404) { + await k8sAppsApi.deleteNamespacedDeployment(name, namespace) + } catch (e: any) { + if (e.response && e.response.statusCode === 404) { return } - throw this.wrapK8sClientError(error) + throw this.wrapK8sClientError(e) } } @@ -1731,6 +1743,55 @@ export class KubeHelper { return this.getAllCustomResources(CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) } + async getAllDevfileWorkspaces(): Promise { + return this.getAllCustomResources(DEVFILE_WORKSPACE_API_GROUP, DEVFILE_WORKSPACE_API_VERSION, DEVFILE_WORKSPACE_KIND_PLURAL) + } + + async deleteAllCustomResources(apiGroup: string, version: string, plural: string): Promise { + const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) + + let resources = await this.getAllCustomResources(apiGroup, version, plural) + for (const resource of resources) { + const name = resource.metadata.name + const namespace = resource.metadata.namespace + try { + await customObjectsApi.deleteNamespacedCustomObject(apiGroup, version, namespace, plural, name, 60) + } catch (e) { + // ignore, check existence later + } + } + + for (let i = 0; i < 12; i++) { + await cli.wait(5 * 1000) + const resources = await this.getAllCustomResources(apiGroup, version, plural) + if (resources.length === 0) { + return + } + } + + // remove finalizers + for (const resource of resources) { + const name = resource.metadata.name + const namespace = resource.metadata.namespace + try { + await this.patchCustomResource(name, namespace, { metadata: { finalizers: null } }, apiGroup, version, plural) + } catch (error) { + if (!await this.getCustomResource(namespace, name, apiGroup, version, plural)) { + continue // successfully removed + } + throw error + } + } + + // wait for some time and check again + await cli.wait(5000) + + resources = await this.getAllCustomResources(apiGroup, version, plural) + if (resources.length !== 0) { + throw new Error(`Failed to remove Custom Resource ${apiGroup}/${version}, ${resources.length} left.`) + } + } + /** * Returns custom resource object by its name in the given namespace. */ @@ -1781,7 +1842,7 @@ export class KubeHelper { try { const { body } = await customObjectsApi.listClusterCustomObject(resourceAPIGroup, resourceAPIVersion, resourcePlural) return (body as any).items ? (body as any).items : [] - } catch (e) { + } catch (e: any) { if (e.response && e.response.statusCode === 404) { // There is no CRD return [] @@ -2145,6 +2206,30 @@ export class KubeHelper { } } + async waitInstalledCSV(namespace: string, subscriptionName: string, timeout = AWAIT_TIMEOUT_S): Promise { + return new Promise(async (resolve, reject) => { + const watcher = new Watch(this.kubeConfig) + const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/subscriptions`, + { fieldSelector: `metadata.name=${subscriptionName}` }, + (_phase: string, obj: any) => { + const subscription = obj as Subscription + if (subscription.status && subscription.status.installedCSV) { + resolve(subscription.status.installedCSV) + } + }, + error => { + if (error) { + reject(error) + } + }) + + setTimeout(() => { + request.abort() + reject(`Timeout reached while waiting for installed CSV of '${subscriptionName}' subscription.`) + }, timeout * 1000) + }) + } + async listOperatorSubscriptions(namespace: string): Promise { const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) try { @@ -2227,7 +2312,7 @@ export class KubeHelper { } } - async waitUntilOperatorIsInstalled(installPlanName: string, namespace: string, timeout = 240) { + async waitOperatorInstallPlan(installPlanName: string, namespace: string, timeout = 240) { return new Promise(async (resolve, reject) => { const watcher = new Watch(this.kubeConfig) const request = await watcher.watch(`/apis/operators.coreos.com/v1alpha1/namespaces/${namespace}/installplans`, diff --git a/src/api/types/olm.ts b/src/api/types/olm.ts index 9cf1c537b..fe84974dc 100644 --- a/src/api/types/olm.ts +++ b/src/api/types/olm.ts @@ -87,11 +87,19 @@ export interface ClusterServiceVersion { kind: string metadata: V1ObjectMeta spec: ClusterServiceVersionSpec + status: ClusterServiceVersionStatus } export interface ClusterServiceVersionSpec { displayName: string install: OperatorInstall + version: string +} + +export interface ClusterServiceVersionStatus { + phase: string + message: string + reason: string } export interface OperatorInstall { diff --git a/src/commands/server/delete.ts b/src/commands/server/delete.ts index 822723cc1..e5e1c8321 100644 --- a/src/commands/server/delete.ts +++ b/src/commands/server/delete.ts @@ -14,18 +14,18 @@ import { Command, flags } from '@oclif/command' import { boolean } from '@oclif/command/lib/flags' import { cli } from 'cli-ux' import * as Listrq from 'listr' -import Listr = require('listr') - +import { OLMDevWorkspaceTasks } from '../../tasks/installers/olm-dev-workspace-operator' import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' import { assumeYes, batch, cheDeployment, cheNamespace, CHE_TELEMETRY, listrRenderer, skipKubeHealthzCheck } from '../../common-flags' -import { DEFAULT_ANALYTIC_HOOK_NAME } from '../../constants' +import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME } from '../../constants' import { CheTasks } from '../../tasks/che' import { DevWorkspaceTasks } from '../../tasks/component-installers/devfile-workspace-operator-installer' import { OLMTasks } from '../../tasks/installers/olm' import { OperatorTasks } from '../../tasks/installers/operator' import { ApiTasks } from '../../tasks/platforms/api' import { findWorkingNamespace, getCommandSuccessMessage, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' +import Listr = require('listr') export default class Delete extends Command { static description = 'delete any Eclipse Che related resource: Kubernetes/OpenShift' @@ -66,8 +66,9 @@ export default class Delete extends Command { const apiTasks = new ApiTasks() const kube = new KubeHelper(flags) const operatorTasks = new OperatorTasks() - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) const cheTasks = new CheTasks(flags) + const olmDevWorkspaceTasks = new OLMDevWorkspaceTasks(flags) const devWorkspaceTasks = new DevWorkspaceTasks(flags) const tasks = new Listrq([], ctx.listrOptions) @@ -79,11 +80,25 @@ export default class Delete extends Command { // Remove devworkspace controller only if there are no more cheClusters after olm/operator tasks tasks.add({ - title: 'Uninstall DevWorkspace Controller', + title: 'Uninstall Dev Workspace Controller', task: async (_ctx: any, task: any) => { const checlusters = await kube.getAllCheClusters() if (checlusters.length === 0) { - return new Listr(devWorkspaceTasks.getUninstallTasks()) + const tasks = new Listr() + + if (await olmDevWorkspaceTasks.isCustomDevWorkspaceCatalogExists()) { + tasks.add(devWorkspaceTasks.deleteDevOperatorCRsAndCRDsTasks()) + tasks.add(olmDevWorkspaceTasks.deleteResourcesTasks()) + tasks.add(devWorkspaceTasks.deleteDevWorkspaceWebhooksTasks(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME)) + } + + if (!await olmDevWorkspaceTasks.isDevWorkspaceOperatorInstalledViaOLM()) { + tasks.add(devWorkspaceTasks.deleteDevOperatorCRsAndCRDsTasks()) + tasks.add(devWorkspaceTasks.deleteResourcesTasks()) + tasks.add(devWorkspaceTasks.deleteDevWorkspaceWebhooksTasks(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE)) + } + + return tasks } task.title = `${task.title}...Skipped: another Eclipse Che deployment found.` }, @@ -97,7 +112,7 @@ export default class Delete extends Command { try { await tasks.run() cli.log(getCommandSuccessMessage()) - } catch (err) { + } catch (err: any) { this.error(wrapCommandError(err)) } } else { diff --git a/src/commands/server/deploy.ts b/src/commands/server/deploy.ts index e34dbca16..50bebd118 100644 --- a/src/commands/server/deploy.ts +++ b/src/commands/server/deploy.ts @@ -365,7 +365,7 @@ export default class Deploy extends Command { preInstallTasks.add(checkChectlAndCheVersionCompatibility(flags)) preInstallTasks.add(downloadTemplates(flags)) preInstallTasks.add({ - title: '🧪 DevWorkspace engine (experimental / technology preview) 🚨', + title: '🧪 DevWorkspace engine', enabled: () => isDevWorkspaceEnabled(ctx) && !ctx.isOpenShift, task: () => new Listr(devWorkspaceTasks.getInstallTasks()), }) diff --git a/src/commands/server/restore.ts b/src/commands/server/restore.ts index 1fddd50ec..ed5dc7509 100644 --- a/src/commands/server/restore.ts +++ b/src/commands/server/restore.ts @@ -23,7 +23,7 @@ import { cheNamespace } from '../../common-flags' import { getBackupServerConfigurationName, parseBackupServerConfig, requestRestore } from '../../api/backup-restore' import { cli } from 'cli-ux' import { ApiTasks } from '../../tasks/platforms/api' -import { TASK_TITLE_CREATE_CUSTOM_CATALOG_SOURCE_FROM_FILE, TASK_TITLE_DELETE_CUSTOM_CATALOG_SOURCE, TASK_TITLE_DELETE_NIGHTLY_CATALOG_SOURCE, OLMTasks, TASK_TITLE_SET_CUSTOM_OPERATOR_IMAGE, TASK_TITLE_PREPARE_CHE_CLUSTER_CR } from '../../tasks/installers/olm' +import { TASK_TITLE_CREATE_CUSTOM_CATALOG_SOURCE_FROM_FILE, TASK_TITLE_DELETE_CUSTOM_CATALOG_SOURCE, TASK_TITLE_DELETE_NEXT_CATALOG_SOURCE, OLMTasks, TASK_TITLE_SET_CUSTOM_OPERATOR_IMAGE, TASK_TITLE_PREPARE_CHE_CLUSTER_CR } from '../../tasks/installers/olm' import { OperatorTasks } from '../../tasks/installers/operator' import { checkChectlAndCheVersionCompatibility, downloadTemplates, TASK_TITLE_CREATE_CHE_CLUSTER_CRD, TASK_TITLE_PATCH_CHECLUSTER_CR } from '../../tasks/installers/common-tasks' import { confirmYN, findWorkingNamespace, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' @@ -365,11 +365,11 @@ export default class Restore extends Command { // All preparations and validations must be done before this task! // Delete old operator if any in case of OLM installer. // For Operator installer, the operator deployment will be downgraded if needed. - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) let olmDeleteTasks = olmTasks.deleteTasks(flags) const tasksToDelete = [ TASK_TITLE_DELETE_CUSTOM_CATALOG_SOURCE, - TASK_TITLE_DELETE_NIGHTLY_CATALOG_SOURCE, + TASK_TITLE_DELETE_NEXT_CATALOG_SOURCE, ] olmDeleteTasks = olmDeleteTasks.filter(task => tasksToDelete.indexOf(task.title) === -1) return new Listr(olmDeleteTasks, ctx.listrOptions) @@ -402,7 +402,7 @@ export default class Restore extends Command { return new Listr(operatorUpdateTasks, ctx.listrOptions) } else { // OLM - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) let olmInstallTasks = olmTasks.startTasks(flags, this) // Remove redundant for restoring tasks const tasksToDelete = [ diff --git a/src/constants.ts b/src/constants.ts index d9755f041..bb950665e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -25,6 +25,9 @@ export const DEFAULT_CHE_OPERATOR_IMAGE_NAME = 'quay.io/eclipse/che-operator' // This image should be updated manually when needed. // Repository location: https://github.com/che-dockerfiles/che-cert-manager-ca-cert-generator-image export const CA_CERT_GENERATION_JOB_IMAGE = 'quay.io/eclipse/che-cert-manager-ca-cert-generator:671342c' +export const INDEX_IMG = 'quay.io/eclipse/eclipse-che-openshift-opm-catalog:next' +export const DEV_WORKSPACE_NEXT_CATALOG_SOURCE_IMAGE = 'quay.io/devfile/devworkspace-operator-index:next' +export const DEV_WORKSPACE_STABLE_CATALOG_SOURCE_IMAGE = 'quay.io/devfile/devworkspace-operator-index:release' export const NEXT_TAG = 'next' @@ -44,8 +47,8 @@ export const LEGACY_CHE_NAMESPACE = 'che' export const DEFAULT_CHE_OLM_PACKAGE_NAME = 'eclipse-che' export const DEFAULT_CHE_OPERATOR_SUBSCRIPTION_NAME = 'eclipse-che-subscription' export const OPERATOR_GROUP_NAME = 'che-operator-group' -export const CVS_PREFIX = 'eclipse-che' -export const DEVWORKSPACE_CVS_PREFIX = 'devworkspace-operator' +export const CSV_PREFIX = 'eclipse-che' +export const DEVWORKSPACE_CSV_PREFIX = 'devworkspace-operator' // OLM channels export const OLM_STABLE_CHANNEL_NAME = 'stable' export const OLM_STABLE_CHANNEL_STARTING_CSV_TEMPLATE = 'eclipse-che.v{{VERSION}}' @@ -57,6 +60,8 @@ export const DEFAULT_OPENSHIFT_OPERATORS_NS_NAME = 'openshift-operators' // OLM catalogs export const CUSTOM_CATALOG_SOURCE_NAME = 'eclipse-che-custom-catalog-source' export const NEXT_CATALOG_SOURCE_NAME = 'eclipse-che-preview' +export const NEXT_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR = 'custom-devworkspace-operator-catalog' +export const STABLE_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR = 'stable-custom-devworkspace-operator-catalog' export const DEFAULT_OLM_SUGGESTED_NAMESPACE = 'eclipse-che' export const KUBERNETES_OLM_CATALOG = 'operatorhubio-catalog' export const OPENSHIFT_OLM_CATALOG = 'community-operators' @@ -96,4 +101,12 @@ export const CHE_CLUSTER_BACKUP_KIND_PLURAL = 'checlusterbackups' export const CHE_CLUSTER_RESTORE_CRD = 'checlusterrestores.org.eclipse.che' export const CHE_CLUSTER_RESTORE_KIND_PLURAL = 'checlusterrestores' +export const DEVFILE_WORKSPACE_API_GROUP = 'workspace.devfile.io' +export const DEVFILE_WORKSPACE_API_VERSION = 'v1alpha2' +export const DEVFILE_WORKSPACE_KIND_PLURAL = 'devworkspaces' + +export const DEVFILE_WORKSPACE_ROUTINGS_API_GROUP = 'controller.devfile.io' +export const DEVFILE_WORKSPACE_ROUTINGS_VERSION = 'v1alpha1' +export const DEVFILE_WORKSPACE_ROUTINGS_KIND_PLURAL = 'devworkspaceroutings' + export const DEFAULT_CHE_TLS_SECRET_NAME = 'che-tls' diff --git a/src/tasks/component-installers/devfile-workspace-operator-installer.ts b/src/tasks/component-installers/devfile-workspace-operator-installer.ts index eb72909a5..e1a9f5ba1 100644 --- a/src/tasks/component-installers/devfile-workspace-operator-installer.ts +++ b/src/tasks/component-installers/devfile-workspace-operator-installer.ts @@ -11,11 +11,10 @@ */ import * as Listr from 'listr' - import { CheHelper } from '../../api/che' import { KubeHelper } from '../../api/kube' import { OpenShiftHelper } from '../../api/openshift' -import { DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, DEVWORKSPACE_CVS_PREFIX } from '../../constants' +import { DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE, DEVFILE_WORKSPACE_API_GROUP, DEVFILE_WORKSPACE_API_VERSION, DEVFILE_WORKSPACE_KIND_PLURAL, DEVFILE_WORKSPACE_ROUTINGS_API_GROUP, DEVFILE_WORKSPACE_ROUTINGS_KIND_PLURAL, DEVFILE_WORKSPACE_ROUTINGS_VERSION } from '../../constants' import { CertManagerTasks } from '../component-installers/cert-manager' /** @@ -31,6 +30,7 @@ export class DevWorkspaceTasks { protected certManagerTask: CertManagerTasks protected devWorkspaceServiceAccount = 'devworkspace-controller-serviceaccount' + protected devWorkspaceWebhookServiceAccount = 'devworkspace-webhook-server' // DevWorkspace Controller Roles protected devWorkspaceLeaderElectionRole = 'devworkspace-controller-leader-election-role' @@ -54,10 +54,14 @@ export class DevWorkspaceTasks { protected devWorkspaceRoleBinding = 'devworkspace-controller-rolebinding' - protected devWorkspaceWebhookServerClusterRolebinding = 'devworkspace-webhook-server' + protected devWorkspaceWebhookServerClusterRole = 'devworkspace-webhook-server' // Deployment names protected deploymentName = 'devworkspace-controller-manager' + protected deploymentWebhookName = 'devworkspace-webhook-server' + + // Services + protected serviceWebhookName = 'devworkspace-webhookserver' // ConfigMap names protected devWorkspaceConfigMap = 'devworkspace-controller-configmap' @@ -75,6 +79,8 @@ export class DevWorkspaceTasks { protected workspaceRoutingsCrdName = 'devworkspaceroutings.controller.devfile.io' + protected devWorkspaceConfigCrdName = 'devworkspaceoperatorconfigs.controller.devfile.io' + protected webhooksName = 'controller.devfile.io' // Web Terminal Operator constants @@ -104,157 +110,190 @@ export class DevWorkspaceTasks { ] } - async isDevWorkspaceInstalledViaOLM(): Promise { - const IsPreInstalledOLM = await this.kubeHelper.isPreInstalledOLM() - if (!IsPreInstalledOLM) { - return false - } - const csvAll = await this.kubeHelper.getClusterServiceVersions(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) - const devWorkspaceCSVs = csvAll.items.filter(csv => csv.metadata.name!.startsWith(DEVWORKSPACE_CVS_PREFIX)) - return devWorkspaceCSVs.length > 0 - } - /** - * Returns list of tasks which uninstall dev-workspace. + * Returns list of tasks which uninstall dev-workspace operator. */ - getUninstallTasks(): ReadonlyArray { + deleteResourcesTasks(): ReadonlyArray { return [ { - title: 'Check DevWorkspace OLM installation', - task: async (ctx: any, task: any) => { - ctx.isDevWorkspaceInstalledViaOLM = Boolean(await this.isDevWorkspaceInstalledViaOLM()) - - task.title = await `${task.title} ...${ctx.isDevWorkspaceInstalledViaOLM ? 'Installed' : 'Not installed'}` - }, - }, - { - title: 'Delete all DevWorkspace Controller deployments', + title: 'Delete all Dev Workspace Controller deployments', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteAllDeployments(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete all DevWorkspace Controller services', + title: 'Delete all Dev Workspace Controller services', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteAllServices(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete all DevWorkspace Controller routes', + title: 'Delete all Dev Workspace Controller routes', enabled: (ctx: any) => !ctx.isOpenShift, task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteAllIngresses(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete all DevWorkspace Controller routes', + title: 'Delete all Dev Workspace Controller routes', enabled: (ctx: any) => ctx.isOpenShift, task: async (_ctx: any, task: any) => { await this.openShiftHelper.deleteAllRoutes(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller configmaps', + title: 'Delete Dev Workspace Controller configmaps', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteConfigMap(this.devWorkspaceConfigMap, DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller ClusterRoleBindings', - task: async (ctx: any, task: any) => { + title: 'Delete Dev Workspace Controller ClusterRoleBindings', + task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteClusterRoleBinding(this.devWorkspaceRoleBinding) await this.kubeHelper.deleteClusterRoleBinding(this.devworkspaceProxyClusterRoleBinding) - if (!ctx.isDevWorkspaceInstalledViaOLM) { - await this.kubeHelper.deleteClusterRoleBinding(this.devWorkspaceWebhookServerClusterRolebinding) - } - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller role', + title: 'Delete Dev Workspace Controller role', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteRole(this.devWorkspaceLeaderElectionRole, DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller roleBinding', + title: 'Delete Dev Workspace Controller roleBinding', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteRoleBinding(this.devWorkspaceLeaderElectionRoleBinding, DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller cluster roles', - task: async (ctx: any, task: any) => { - if (!ctx.isDevWorkspaceInstalledViaOLM) { - await this.kubeHelper.deleteClusterRole(this.devWorkspaceEditWorkspaceClusterRole) - await this.kubeHelper.deleteClusterRole(this.devWorkspaceViewWorkspaceClusterRole) - await this.kubeHelper.deleteClusterRole(this.devWorkspaceClusterRoleWebhook) - } - + title: 'Delete Dev Workspace Controller cluster roles', + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteClusterRole(this.devWorkspaceEditWorkspaceClusterRole) + await this.kubeHelper.deleteClusterRole(this.devWorkspaceViewWorkspaceClusterRole) await this.kubeHelper.deleteClusterRole(this.devworkspaceProxyClusterRole) await this.kubeHelper.deleteClusterRole(this.devworkspaceClusterRole) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller service account', + title: 'Delete Dev Workspace Controller service account', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteServiceAccount(this.devWorkspaceServiceAccount, DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller self-signed certificates', + title: 'Delete Dev Workspace Controller self-signed certificates', enabled: async (ctx: any) => !ctx.IsOpenshift, task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteNamespacedCertificate(this.devWorkspaceCertificate, 'v1', DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) await this.kubeHelper.deleteNamespacedIssuer(this.devWorkspaceCertIssuer, 'v1', DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - task.title = await `${task.title}...OK` + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller webhooks configurations', - enabled: ctx => !ctx.isDevWorkspaceInstalledViaOLM, + title: 'Delete DevWorkspace Operator Namespace', task: async (_ctx: any, task: any) => { - await this.kubeHelper.deleteMutatingWebhookConfiguration(this.webhooksName) - await this.kubeHelper.deleteValidatingWebhookConfiguration(this.webhooksName) + const namespaceExist = await this.kubeHelper.getNamespace(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) + if (namespaceExist) { + await this.kubeHelper.deleteNamespace(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) + } + task.title = `${task.title}...[OK]` + }, + }, + ] + } - task.title = await `${task.title} ...OK` + deleteDevOperatorCRsAndCRDsTasks(): ReadonlyArray { + return [ + { + title: `Delete ${DEVFILE_WORKSPACE_API_GROUP}/${DEVFILE_WORKSPACE_API_VERSION} resources`, + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteAllCustomResources(DEVFILE_WORKSPACE_API_GROUP, DEVFILE_WORKSPACE_API_VERSION, DEVFILE_WORKSPACE_KIND_PLURAL) + task.title = `${task.title}...[OK]` + }, + }, + { + title: `Delete ${DEVFILE_WORKSPACE_ROUTINGS_API_GROUP}/${DEVFILE_WORKSPACE_ROUTINGS_VERSION} resources`, + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteAllCustomResources(DEVFILE_WORKSPACE_ROUTINGS_API_GROUP, DEVFILE_WORKSPACE_ROUTINGS_VERSION, DEVFILE_WORKSPACE_ROUTINGS_KIND_PLURAL) + task.title = `${task.title}...[OK]` }, }, { - title: 'Delete DevWorkspace Controller CRDs', - enabled: ctx => !ctx.isDevWorkspaceInstalledViaOLM, + title: 'Delete Dev Workspace CRDs', task: async (_ctx: any, task: any) => { await this.kubeHelper.deleteCrd(this.devWorkspacesCrdName) await this.kubeHelper.deleteCrd(this.devWorkspaceTemplatesCrdName) await this.kubeHelper.deleteCrd(this.workspaceRoutingsCrdName) + await this.kubeHelper.deleteCrd(this.devWorkspaceConfigCrdName) - task.title = await `${task.title}...OK` + task.title = await `${task.title}...[OK]` }, }, + ] + } + + deleteDevWorkspaceWebhooksTasks(namespace: string): ReadonlyArray { + return [ { - title: 'Delete DevWorkspace Operator Namespace', + title: 'Delete Dev Workspace webhooks deployment', task: async (_ctx: any, task: any) => { - const namespaceExist = await this.kubeHelper.getNamespace(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - if (namespaceExist) { - await this.kubeHelper.deleteNamespace(DEFAULT_DEV_WORKSPACE_CONTROLLER_NAMESPACE) - } - task.title = `${task.title}...OK` + await this.kubeHelper.deleteDeployment(namespace, this.deploymentWebhookName) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete all Dev Workspace webhooks services', + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteService(this.serviceWebhookName, namespace) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace webhook Cluster RoleBinding', + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteClusterRoleBinding(this.devWorkspaceWebhookServerClusterRole) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace webhook Cluster Role', + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteClusterRole(this.devWorkspaceWebhookServerClusterRole) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete DevWorkspace webhooks service account', + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteServiceAccount(this.devWorkspaceWebhookServiceAccount, namespace) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace webhooks configurations', + enabled: ctx => !ctx.isOLMStableDevWorkspaceOperator && !ctx.devWorkspacesPresent, + task: async (_ctx: any, task: any) => { + await this.kubeHelper.deleteMutatingWebhookConfiguration(this.webhooksName) + await this.kubeHelper.deleteValidatingWebhookConfiguration(this.webhooksName) + task.title = `${task.title} ...[OK]` }, }, ] diff --git a/src/tasks/installers/installer.ts b/src/tasks/installers/installer.ts index 01a01b230..5dc68af33 100644 --- a/src/tasks/installers/installer.ts +++ b/src/tasks/installers/installer.ts @@ -24,7 +24,7 @@ import { OperatorTasks } from './operator' export class InstallerTasks { updateTasks(flags: any, command: Command): ReadonlyArray { const operatorTasks = new OperatorTasks() - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) let title: string let task: any @@ -54,7 +54,7 @@ export class InstallerTasks { preUpdateTasks(flags: any, command: Command): ReadonlyArray { const operatorTasks = new OperatorTasks() - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) let title: string let task: any @@ -86,7 +86,7 @@ export class InstallerTasks { const ctx = ChectlContext.get() const operatorTasks = new OperatorTasks() - const olmTasks = new OLMTasks() + const olmTasks = new OLMTasks(flags) let title: string let task: any diff --git a/src/tasks/installers/olm-dev-workspace-operator.ts b/src/tasks/installers/olm-dev-workspace-operator.ts new file mode 100644 index 000000000..39ac4c55f --- /dev/null +++ b/src/tasks/installers/olm-dev-workspace-operator.ts @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2019-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import Command from '@oclif/command' +import { DevWorkspaceContextKeys, OLMInstallationUpdate } from '../../api/context' +import { KubeHelper } from '../../api/kube' +import { CatalogSource, Subscription } from '../../api/types/olm' +import { VersionHelper } from '../../api/version' +import { DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, DEVWORKSPACE_CSV_PREFIX, DEV_WORKSPACE_NEXT_CATALOG_SOURCE_IMAGE, DEV_WORKSPACE_STABLE_CATALOG_SOURCE_IMAGE, NEXT_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR, STABLE_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR } from '../../constants' +import Listr = require('listr') + +export class OLMDevWorkspaceTasks { + private readonly DEV_WORKSPACE_OPERATOR_SUBSCRIPTION = 'devworkspace-operator' + + private readonly OLM_CHANNEL = 'fast' + + private readonly OLM_PACKAGE_NAME = 'devworkspace-operator' + private readonly kube: KubeHelper + + constructor(flags: any) { + this.kube = new KubeHelper(flags) + } + + startTasks(flags: any, _command: Command): Listr.ListrTask[] { + return [ + { + title: 'Check Dev Workspace operator installation', + task: async (ctx: any, task: any) => { + ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB] = await this.isDevWorkspaceOperatorInstalledViaOLM() && !await this.isCustomDevWorkspaceCatalogExists() + task.title = `${task.title}...${ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB] ? '[OperatorHub]' : '[Not OperatorHub]'}` + }, + }, + { + title: 'Create Dev Workspace operator CatalogSource', + enabled: ctx => !ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB], + task: async (ctx: any, task: any) => { + ctx[DevWorkspaceContextKeys.CATALOG_SOURCE_NAME] = VersionHelper.isDeployingStableVersion(flags) ? STABLE_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR : NEXT_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR + const catalogSourceImage = VersionHelper.isDeployingStableVersion(flags) ? DEV_WORKSPACE_STABLE_CATALOG_SOURCE_IMAGE : DEV_WORKSPACE_NEXT_CATALOG_SOURCE_IMAGE + + if (!await this.kube.IsCatalogSourceExists(ctx[DevWorkspaceContextKeys.CATALOG_SOURCE_NAME], DEFAULT_OPENSHIFT_OPERATORS_NS_NAME)) { + const catalogSource = this.constructCatalogSource(ctx[DevWorkspaceContextKeys.CATALOG_SOURCE_NAME], catalogSourceImage) + await this.kube.createCatalogSource(catalogSource) + await this.kube.waitCatalogSource(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, ctx[DevWorkspaceContextKeys.CATALOG_SOURCE_NAME]) + task.title = `${task.title}...[OK]` + } else { + task.title = `${task.title}...[Exists]` + } + }, + }, + { + title: 'Create Dev Workspace operator Subscription', + enabled: ctx => !ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB], + task: async (ctx: any, task: any) => { + const subscription = await this.kube.getOperatorSubscription(this.DEV_WORKSPACE_OPERATOR_SUBSCRIPTION, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + if (!subscription) { + const subscription = this.constructSubscription(this.DEV_WORKSPACE_OPERATOR_SUBSCRIPTION, ctx[DevWorkspaceContextKeys.CATALOG_SOURCE_NAME]) + await this.kube.createOperatorSubscription(subscription) + task.title = `${task.title}...[OK]` + } else { + task.title = `${task.title}...[Exists]` + } + }, + }, + { + title: 'Wait Dev Workspace operator Subscription is ready', + enabled: ctx => !ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB], + task: async (ctx: any, task: any) => { + const installPlan = await this.kube.waitOperatorSubscriptionReadyForApproval(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, this.DEV_WORKSPACE_OPERATOR_SUBSCRIPTION, 600) + ctx[DevWorkspaceContextKeys.INSTALL_PLAN] = installPlan.name + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Wait Dev Workspace operator InstallPlan', + enabled: ctx => !ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB], + task: async (ctx: any, task: any) => { + await this.kube.waitOperatorInstallPlan(ctx[DevWorkspaceContextKeys.INSTALL_PLAN], DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Wait Dev Workspace CSV', + enabled: ctx => !ctx[DevWorkspaceContextKeys.IS_DEV_WORKSPACE_INSTALLED_VIA_OPERATOR_HUB], + task: async (ctx: any, task: any) => { + const installedCSV = await this.kube.waitInstalledCSV(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, this.DEV_WORKSPACE_OPERATOR_SUBSCRIPTION) + const csv = await this.kube.getCSV(installedCSV, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + if (!csv) { + throw new Error(`Cluster service version resource ${installedCSV} not found`) + } + if (csv.status.phase === 'Failed') { + throw new Error(`Cluster service version resource failed for Dev Workspace operator, cause: ${csv.status.message}, reason: ${csv.status.reason}.`) + } + task.title = `${task.title}...[OK]` + }, + }, + ] + } + + deleteResourcesTasks(): ReadonlyArray { + return [ + { + title: 'Delete Dev Workspace operator subscription', + task: async (_ctx: any, task: any) => { + await this.kube.deleteOperatorSubscription(this.DEV_WORKSPACE_OPERATOR_SUBSCRIPTION, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace operator CSV', + task: async (_ctx: any, task: any) => { + const csvs = await this.kube.getClusterServiceVersions(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + const csvsToDelete = csvs.items.filter(csv => csv.metadata.name!.startsWith(DEVWORKSPACE_CSV_PREFIX)) + for (const csv of csvsToDelete) { + await this.kube.deleteClusterServiceVersion(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, csv.metadata.name!) + } + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace operator catalog source for \'next\' channel', + task: async (_ctx: any, task: any) => { + await this.kube.deleteCatalogSource(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, NEXT_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Delete Dev Workspace operator catalog source for \'stable\' channel', + task: async (_ctx: any, task: any) => { + await this.kube.deleteCatalogSource(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, STABLE_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR) + task.title = `${task.title}...[OK]` + }, + }, + ] + } + + private constructCatalogSource(name: string, image: string): CatalogSource { + return { + apiVersion: 'operators.coreos.com/v1alpha1', + kind: 'CatalogSource', + metadata: { + name, + namespace: DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, + }, + spec: { + image, + sourceType: 'grpc', + updateStrategy: { + registryPoll: { + interval: '15m', + }, + }, + }, + } + } + + private constructSubscription(name: string, source: string): Subscription { + return { + apiVersion: 'operators.coreos.com/v1alpha1', + kind: 'Subscription', + metadata: { + name, + namespace: DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, + }, + spec: { + channel: this.OLM_CHANNEL, + installPlanApproval: OLMInstallationUpdate.AUTO, + name: this.OLM_PACKAGE_NAME, + source, + sourceNamespace: DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, + }, + } + } + + async isCustomDevWorkspaceCatalogExists(): Promise { + const IsPreInstalledOLM = await this.kube.isPreInstalledOLM() + if (!IsPreInstalledOLM) { + return false + } + + const isNextCatalogExists = await this.kube.IsCatalogSourceExists(STABLE_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + const isStableCatalogExists = await this.kube.IsCatalogSourceExists(NEXT_CATALOG_SOURCE_DEV_WORKSPACE_OPERATOR, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + + return isNextCatalogExists || isStableCatalogExists + } + + async isDevWorkspaceOperatorInstalledViaOLM(): Promise { + const IsPreInstalledOLM = await this.kube.isPreInstalledOLM() + if (!IsPreInstalledOLM) { + return false + } + + const csvAll = await this.kube.getClusterServiceVersions(DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) + const devWorkspaceCSVs = csvAll.items.filter(csv => csv.metadata.name!.startsWith(DEVWORKSPACE_CSV_PREFIX)) + return devWorkspaceCSVs.length > 0 + } +} diff --git a/src/tasks/installers/olm.ts b/src/tasks/installers/olm.ts index 905a82220..5d7bb223b 100644 --- a/src/tasks/installers/olm.ts +++ b/src/tasks/installers/olm.ts @@ -20,31 +20,35 @@ import { CheHelper } from '../../api/che' import { KubeHelper } from '../../api/kube' import { CatalogSource, Subscription } from '../../api/types/olm' import { VersionHelper } from '../../api/version' -import { CUSTOM_CATALOG_SOURCE_NAME, CVS_PREFIX, DEFAULT_CHE_NAMESPACE, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_OLM_KUBERNETES_NAMESPACE, DEFAULT_OPENSHIFT_MARKET_PLACE_NAMESPACE, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, KUBERNETES_OLM_CATALOG, NEXT_CATALOG_SOURCE_NAME, OLM_NEXT_CHANNEL_NAME, OLM_STABLE_CHANNEL_NAME, OPENSHIFT_OLM_CATALOG, OPERATOR_GROUP_NAME, DEFAULT_CHE_OPERATOR_SUBSCRIPTION_NAME, OLM_STABLE_CHANNEL_STARTING_CSV_TEMPLATE } from '../../constants' +import { CUSTOM_CATALOG_SOURCE_NAME, CSV_PREFIX, DEFAULT_CHE_NAMESPACE, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_OLM_KUBERNETES_NAMESPACE, DEFAULT_OPENSHIFT_MARKET_PLACE_NAMESPACE, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, KUBERNETES_OLM_CATALOG, NEXT_CATALOG_SOURCE_NAME, OLM_NEXT_CHANNEL_NAME, OLM_STABLE_CHANNEL_NAME, OPENSHIFT_OLM_CATALOG, OPERATOR_GROUP_NAME, DEFAULT_CHE_OPERATOR_SUBSCRIPTION_NAME, OLM_STABLE_CHANNEL_STARTING_CSV_TEMPLATE, INDEX_IMG } from '../../constants' import { getEmbeddedTemplatesDirectory, isKubernetesPlatformFamily } from '../../util' import { createEclipseCheCluster, patchingEclipseCheCluster } from './common-tasks' +import { OLMDevWorkspaceTasks } from './olm-dev-workspace-operator' export const TASK_TITLE_SET_CUSTOM_OPERATOR_IMAGE = 'Set custom operator image' export const TASK_TITLE_CREATE_CUSTOM_CATALOG_SOURCE_FROM_FILE = 'Create custom catalog source from file' export const TASK_TITLE_PREPARE_CHE_CLUSTER_CR = 'Prepare Eclipse Che cluster CR' export const TASK_TITLE_DELETE_CUSTOM_CATALOG_SOURCE = `Delete(OLM) custom catalog source ${CUSTOM_CATALOG_SOURCE_NAME}` -export const TASK_TITLE_DELETE_NIGHTLY_CATALOG_SOURCE = `Delete(OLM) nigthly catalog source ${NEXT_CATALOG_SOURCE_NAME}` +export const TASK_TITLE_DELETE_NEXT_CATALOG_SOURCE = `Delete(OLM) nigthly catalog source ${NEXT_CATALOG_SOURCE_NAME}` export class OLMTasks { - prometheusRoleName = 'prometheus-k8s' - prometheusRoleBindingName = 'prometheus-k8s' + private readonly prometheusRoleName = 'prometheus-k8s' + private readonly prometheusRoleBindingName = 'prometheus-k8s' + private readonly kube: KubeHelper + private readonly che: CheHelper + private readonly olmDevWorkspaceTasks: OLMDevWorkspaceTasks - /** - * Returns list of tasks which perform preflight platform checks. - */ - startTasks(flags: any, command: Command): Listr.ListrTask[] { - const kube = new KubeHelper(flags) - const che = new CheHelper(flags) + constructor(flags: any) { + this.kube = new KubeHelper(flags) + this.che = new CheHelper(flags) + this.olmDevWorkspaceTasks = new OLMDevWorkspaceTasks(flags) + } + startTasks(flags: any, command: Command): Listr.ListrTask[] { return [ - this.isOlmPreInstalledTask(command, kube), + this.isOlmPreInstalledTask(command), { title: 'Configure context information', task: async (ctx: any, task: any) => { @@ -87,11 +91,11 @@ export class OLMTasks { enabled: () => flags['cluster-monitoring'] && flags.platform === 'openshift', title: `Create Role ${this.prometheusRoleName} in namespace ${flags.chenamespace}`, task: async (_ctx: any, task: any) => { - if (await kube.isRoleExist(this.prometheusRoleName, flags.chenamespace)) { + if (await this.kube.isRoleExist(this.prometheusRoleName, flags.chenamespace)) { task.title = `${task.title}...[Exists]` } else { const yamlFilePath = path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', 'prometheus-role.yaml') - await kube.createRoleFromFile(yamlFilePath, flags.chenamespace) + await this.kube.createRoleFromFile(yamlFilePath, flags.chenamespace) task.title = `${task.title}...[OK].` } }, @@ -100,11 +104,11 @@ export class OLMTasks { enabled: () => flags['cluster-monitoring'] && flags.platform === 'openshift', title: `Create RoleBinding ${this.prometheusRoleBindingName} in namespace ${flags.chenamespace}`, task: async (_ctx: any, task: any) => { - if (await kube.isRoleBindingExist(this.prometheusRoleBindingName, flags.chenamespace)) { + if (await this.kube.isRoleBindingExist(this.prometheusRoleBindingName, flags.chenamespace)) { task.title = `${task.title}...[Exists]` } else { const yamlFilePath = path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', 'prometheus-role-binding.yaml') - await kube.createRoleBindingFromFile(yamlFilePath, flags.chenamespace) + await this.kube.createRoleBindingFromFile(yamlFilePath, flags.chenamespace) task.title = `${task.title}...[OK]` } }, @@ -113,39 +117,39 @@ export class OLMTasks { title: 'Create operator group', enabled: (ctx: any) => ctx.operatorNamespace !== DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, task: async (_ctx: any, task: any) => { - if (await che.findCheOperatorOperatorGroup(flags.chenamespace)) { + if (await this.che.findCheOperatorOperatorGroup(flags.chenamespace)) { task.title = `${task.title}...[Exists]` } else { - await kube.createOperatorGroup(OPERATOR_GROUP_NAME, flags.chenamespace) + await this.kube.createOperatorGroup(OPERATOR_GROUP_NAME, flags.chenamespace) task.title = `${task.title}...[OK]` } }, }, { - enabled: () => !VersionHelper.isDeployingStableVersion(flags) && !flags[OLM.CATALOG_SOURCE_NAME] && !flags[OLM.CATALOG_SOURCE_YAML] && flags[OLM.CHANNEL] !== OLM_STABLE_CHANNEL_NAME, - title: 'Create next index CatalogSource', + enabled: () => !flags[OLM.CATALOG_SOURCE_NAME] && !flags[OLM.CATALOG_SOURCE_YAML] && flags[OLM.CHANNEL] !== OLM_STABLE_CHANNEL_NAME, + title: 'Create CatalogSource for \'next\' channel', task: async (ctx: any, task: any) => { - if (!await kube.IsCatalogSourceExists(NEXT_CATALOG_SOURCE_NAME, ctx.operatorNamespace)) { - const catalogSourceImage = `quay.io/eclipse/eclipse-che-${ctx.generalPlatformName}-opm-catalog:next` - const nextCatalogSource = this.constructIndexCatalogSource(ctx.operatorNamespace, catalogSourceImage) - await kube.createCatalogSource(nextCatalogSource) - await kube.waitCatalogSource(ctx.operatorNamespace, NEXT_CATALOG_SOURCE_NAME) + if (!await this.kube.IsCatalogSourceExists(NEXT_CATALOG_SOURCE_NAME, ctx.operatorNamespace)) { + const nextCatalogSource = this.constructNextCatalogSource(ctx.operatorNamespace) + await this.kube.createCatalogSource(nextCatalogSource) + await this.kube.waitCatalogSource(ctx.operatorNamespace, NEXT_CATALOG_SOURCE_NAME) task.title = `${task.title}...[OK]` } else { task.title = `${task.title}...[Exists]` } }, }, + ...this.olmDevWorkspaceTasks.startTasks(flags, command), { title: TASK_TITLE_CREATE_CUSTOM_CATALOG_SOURCE_FROM_FILE, enabled: () => flags[OLM.CATALOG_SOURCE_YAML], task: async (ctx: any, task: any) => { - const customCatalogSource: CatalogSource = kube.readCatalogSourceFromFile(flags[OLM.CATALOG_SOURCE_YAML]) - if (!await kube.IsCatalogSourceExists(customCatalogSource.metadata!.name!, flags.chenamespace)) { + const customCatalogSource: CatalogSource = this.kube.readCatalogSourceFromFile(flags[OLM.CATALOG_SOURCE_YAML]) + if (!await this.kube.IsCatalogSourceExists(customCatalogSource.metadata!.name!, flags.chenamespace)) { customCatalogSource.metadata.name = ctx.sourceName customCatalogSource.metadata.namespace = flags.chenamespace - await kube.createCatalogSource(customCatalogSource) - await kube.waitCatalogSource(flags.chenamespace, CUSTOM_CATALOG_SOURCE_NAME) + await this.kube.createCatalogSource(customCatalogSource) + await this.kube.waitCatalogSource(flags.chenamespace, CUSTOM_CATALOG_SOURCE_NAME) task.title = `${task.title}...[OK: ${CUSTOM_CATALOG_SOURCE_NAME}]` } else { task.title = `${task.title}...[Exists]` @@ -155,7 +159,7 @@ export class OLMTasks { { title: 'Create operator subscription', task: async (ctx: any, task: any) => { - let subscription = await che.findCheOperatorSubscription(ctx.operatorNamespace) + let subscription = await this.che.findCheOperatorSubscription(ctx.operatorNamespace) if (subscription) { ctx.subscriptionName = subscription.metadata.name task.title = `${task.title}...[Exists]` @@ -177,14 +181,14 @@ export class OLMTasks { } else { throw new Error(`Unknown OLM channel ${flags[OLM.CHANNEL]}`) } - await kube.createOperatorSubscription(subscription) + await this.kube.createOperatorSubscription(subscription) task.title = `${task.title}...[OK]` }, }, { title: 'Wait while subscription is ready', task: async (ctx: any, task: any) => { - const installPlan = await kube.waitOperatorSubscriptionReadyForApproval(ctx.operatorNamespace, ctx.subscriptionName, 600) + const installPlan = await this.kube.waitOperatorSubscriptionReadyForApproval(ctx.operatorNamespace, ctx.subscriptionName, 600) ctx.installPlanName = installPlan.name task.title = `${task.title}...[OK]` }, @@ -193,7 +197,28 @@ export class OLMTasks { title: 'Approve installation', enabled: ctx => ctx.approvalStrategy === OLMInstallationUpdate.MANUAL, task: async (ctx: any, task: any) => { - await kube.approveOperatorInstallationPlan(ctx.installPlanName, ctx.operatorNamespace) + await this.kube.approveOperatorInstallationPlan(ctx.installPlanName, ctx.operatorNamespace) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Wait operator install plan', + task: async (ctx: any, task: any) => { + await this.kube.waitOperatorInstallPlan(ctx.installPlanName, ctx.operatorNamespace) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Check cluster service version resource', + task: async (ctx: any, task: any) => { + const installedCSV = await this.kube.waitInstalledCSV(ctx.operatorNamespace, ctx.subscriptionName) + const csv = await this.kube.getCSV(installedCSV, ctx.operatorNamespace) + if (!csv) { + throw new Error(`cluster service version resource ${installedCSV} not found`) + } + if (csv.status.phase === 'Failed') { + throw new Error(`cluster service version resource failed. Cause: ${csv.status.message}. Reason: ${csv.status.reason}.`) + } task.title = `${task.title}...[OK]` }, }, @@ -201,56 +226,65 @@ export class OLMTasks { title: TASK_TITLE_SET_CUSTOM_OPERATOR_IMAGE, enabled: () => flags['che-operator-image'], task: async (_ctx: any, task: any) => { - const csvList = await kube.getClusterServiceVersions(flags.chenamespace) + const csvList = await this.kube.getClusterServiceVersions(flags.chenamespace) if (csvList.items.length < 1) { throw new Error('Failed to get CSV for Che operator') } const csv = csvList.items[0] const jsonPatch = [{ op: 'replace', path: '/spec/install/spec/deployments/0/spec/template/spec/containers/0/image', value: flags['che-operator-image'] }] - await kube.patchClusterServiceVersion(csv.metadata.namespace!, csv.metadata.name!, jsonPatch) - task.title = `${task.title}...[OK]` - }, - }, - { - title: 'Wait while operator installed', - task: async (ctx: any, task: any) => { - await kube.waitUntilOperatorIsInstalled(ctx.installPlanName, ctx.operatorNamespace) + await this.kube.patchClusterServiceVersion(csv.metadata.namespace!, csv.metadata.name!, jsonPatch) task.title = `${task.title}...[OK]` }, }, { title: TASK_TITLE_PREPARE_CHE_CLUSTER_CR, task: async (ctx: any, task: any) => { - const cheCluster = await kube.getCheCluster(flags.chenamespace) + const cheCluster = await this.kube.getCheCluster(flags.chenamespace) if (cheCluster) { task.title = `${task.title}...[Exists]` return } if (!ctx.customCR) { - ctx.defaultCR = await this.getCRFromCSV(kube, ctx.operatorNamespace, ctx.subscriptionName) + ctx.defaultCR = await this.getCRFromCSV(ctx.operatorNamespace, ctx.subscriptionName) } task.title = `${task.title}...[OK]` }, }, - createEclipseCheCluster(flags, kube), + createEclipseCheCluster(flags, this.kube), ] } preUpdateTasks(flags: any, command: Command): Listr { - const kube = new KubeHelper(flags) - const che = new CheHelper(flags) return new Listr([ - this.isOlmPreInstalledTask(command, kube), + this.isOlmPreInstalledTask(command), { - title: 'Check if operator subscription exists', - task: async (ctx: any, task: any) => { - const subscription = await che.findCheOperatorSubscription(flags.chenamespace) + title: 'Check operator subscription', + task: async (ctx: any, task: Listr.ListrTaskWrapper) => { + const subscription = await this.che.findCheOperatorSubscription(flags.chenamespace) if (!subscription) { command.error('Unable to find operator subscription') } ctx.operatorNamespace = subscription.metadata.namespace + ctx.installPlanApproval = subscription.spec.installPlanApproval + + if (subscription.spec.installPlanApproval === OLMInstallationUpdate.AUTO) { + task.title = `${task.title}...[Interrupted]` + return new Listr([ + { + title: '[Warning] OLM itself manage operator update with installation mode \'Automatic\'.', + task: () => {}, + }, + { + title: '[Warning] Use \'chectl server:update\' command only with \'Manual\' installation plan approval.', + task: () => { + command.exit(0) + }, + }, + ], ctx.listrOptions) + } + task.title = `${task.title}...[OK]` }, }, @@ -258,7 +292,7 @@ export class OLMTasks { title: 'Check if CheCluster CR exists', task: async (ctx: any, _task: any) => { if (ctx.operatorNamespace === DEFAULT_OPENSHIFT_OPERATORS_NS_NAME) { - const cheClusters = await kube.getAllCheClusters() + const cheClusters = await this.kube.getAllCheClusters() if (cheClusters.length === 0) { command.error(`Eclipse Che cluster CR was not found in the namespace '${flags.chenamespace}'`) } @@ -267,7 +301,7 @@ export class OLMTasks { } ctx.checlusterNamespace = cheClusters[0].metadata.namespace } else { - const cheCluster = await kube.getCheCluster(ctx.operatorNamespace) + const cheCluster = await this.kube.getCheCluster(ctx.operatorNamespace) if (!cheCluster) { command.error(`Eclipse Che cluster CR was not found in the namespace '${flags.chenamespace}'`) } @@ -330,7 +364,7 @@ export class OLMTasks { title: 'Wait while newer operator installed', enabled: (ctx: any) => ctx.installPlanName, task: async (ctx: any, task: any) => { - await kube.waitUntilOperatorIsInstalled(ctx.installPlanName, ctx.operatorNamespace, 60) + await kube.waitOperatorInstallPlan(ctx.installPlanName, ctx.operatorNamespace, 60) ctx.highlightedMessages.push(`Operator is updated from ${ctx.currentVersion} to ${ctx.nextVersion} version`) task.title = `${task.title}...[OK]` }, @@ -367,7 +401,7 @@ export class OLMTasks { }, }, { - title: 'Delete(OLM) operator subscription', + title: 'Delete operator subscription', enabled: ctx => ctx.isPreInstalledOLM && ctx.subscriptionName, task: async (ctx: any, task: any) => { await kube.deleteOperatorSubscription(ctx.subscriptionName, ctx.operatorNamespace) @@ -375,11 +409,11 @@ export class OLMTasks { }, }, { - title: 'Delete(OLM) Eclipse Che cluster service versions', + title: 'Delete Eclipse Che cluster service versions', enabled: ctx => ctx.isPreInstalledOLM, task: async (ctx: any, task: any) => { const csvs = await kube.getClusterServiceVersions(ctx.operatorNamespace) - const csvsToDelete = csvs.items.filter(csv => csv.metadata.name!.startsWith(CVS_PREFIX)) + const csvsToDelete = csvs.items.filter(csv => csv.metadata.name!.startsWith(CSV_PREFIX)) for (const csv of csvsToDelete) { await kube.deleteClusterServiceVersion(ctx.operatorNamespace, csv.metadata.name!) } @@ -387,7 +421,7 @@ export class OLMTasks { }, }, { - title: 'Delete(OLM) operator group', + title: 'Delete operator group', // Do not delete global operator group if operator is in all namespaces mode enabled: ctx => ctx.isPreInstalledOLM && ctx.operatorNamespace !== DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, task: async (ctx: any, task: any) => { @@ -406,7 +440,7 @@ export class OLMTasks { }, }, { - title: TASK_TITLE_DELETE_NIGHTLY_CATALOG_SOURCE, + title: TASK_TITLE_DELETE_NEXT_CATALOG_SOURCE, task: async (ctx: any, task: any) => { await kube.deleteCatalogSource(ctx.operatorNamespace, NEXT_CATALOG_SOURCE_NAME) task.title = `${task.title}...[OK]` @@ -429,11 +463,11 @@ export class OLMTasks { ] } - private isOlmPreInstalledTask(command: Command, kube: KubeHelper): Listr.ListrTask { + private isOlmPreInstalledTask(command: Command): Listr.ListrTask { return { title: 'Check if OLM is pre-installed on the platform', task: async (_ctx: any, task: any) => { - if (!await kube.isPreInstalledOLM()) { + if (!await this.kube.isPreInstalledOLM()) { cli.warn('Looks like your platform hasn\'t got embedded OLM, so you should install it manually. For quick start you can use:') cli.url('install.sh', 'https://raw.githubusercontent.com/operator-framework/operator-lifecycle-manager/master/deploy/upstream/quickstart/install.sh') command.error('OLM is required for installation of Eclipse Che with installer flag \'olm\'') @@ -462,7 +496,7 @@ export class OLMTasks { } } - private constructIndexCatalogSource(namespace: string, catalogSourceImage: string): CatalogSource { + private constructNextCatalogSource(namespace: string): CatalogSource { return { apiVersion: 'operators.coreos.com/v1alpha1', kind: 'CatalogSource', @@ -471,7 +505,7 @@ export class OLMTasks { namespace, }, spec: { - image: catalogSourceImage, + image: INDEX_IMG, sourceType: 'grpc', updateStrategy: { registryPoll: { @@ -482,18 +516,19 @@ export class OLMTasks { } } - private async getCRFromCSV(kube: KubeHelper, namespace: string, subscriptionName: string): Promise { - const subscription = await kube.getOperatorSubscription(subscriptionName, namespace) + private async getCRFromCSV(namespace: string, subscriptionName: string): Promise { + const subscription = await this.kube.getOperatorSubscription(subscriptionName, namespace) if (!subscription) { throw new Error(`Subscription '${subscriptionName}' not found in namespace '${namespace}'`) } const installedCSV = subscription.status!.installedCSV! - const csv = await kube.getCSV(installedCSV, namespace) + const csv = await this.kube.getCSV(installedCSV, namespace) + if (csv && csv.metadata.annotations) { const CRRaw = csv.metadata.annotations!['alm-examples'] return (yaml.load(CRRaw) as Array).find(cr => cr.kind === 'CheCluster') } else { - throw new Error(`Unable to retrieve Che cluster CR definition from CSV: ${installedCSV}`) + throw new Error(`Unable to retrieve CheCluster CR ${!csv ? '' : 'from CSV: ' + csv.spec.displayName}`) } } } diff --git a/src/tasks/installers/operator.ts b/src/tasks/installers/operator.ts index 9ee285d05..4f5bdd773 100644 --- a/src/tasks/installers/operator.ts +++ b/src/tasks/installers/operator.ts @@ -484,7 +484,7 @@ export class OperatorTasks { try { await kh.patchCustomResource(checluster.metadata.name, flags.chenamespace, { metadata: { finalizers: null } }, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_KIND_PLURAL) } catch (error) { - if (await kh.getCheCluster(flags.chenamespace)) { + if (!await kh.getCheCluster(flags.chenamespace)) { task.title = `${task.title}...OK` return // successfully removed } diff --git a/test/e2e/e2e.test.ts b/test/e2e/e2e.test.ts index c73a2d6b1..fb07cb7b0 100644 --- a/test/e2e/e2e.test.ts +++ b/test/e2e/e2e.test.ts @@ -83,7 +83,7 @@ describe('Get Eclipse Che server status', () => { describe('Stop Eclipse Che server', () => { it('server:stop command', async () => { - const { exitCode, stdout, stderr } = await execa(binChectl, ['server:delete', `-n ${NAMESPACE}`, '--telemetry=off', '--delete-namespace', '--yes'], { shell: true }) + const { exitCode, stdout, stderr } = await execa(binChectl, ['server:stop', `-n ${NAMESPACE}`, '--telemetry=off'], { shell: true }) console.log(`stdout: ${stdout}`) console.log(`stderr: ${stderr}`)