From af390a5f013561456dfd42db652bc663e08178ac Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 8 Oct 2021 20:23:48 +0300 Subject: [PATCH] Deploy dex on minikube (#1719) * feat: configure Dex on minikube Signed-off-by: Anatolii Bazko --- package.json | 4 +- {installers => resources}/cert-manager.yml | 0 resources/dex/cluster-role-binding.yaml | 15 + resources/dex/cluster-role.yaml | 14 + resources/dex/configmap.yaml | 32 ++ resources/dex/deployment.yaml | 45 +++ resources/dex/ingress.yaml | 30 ++ resources/dex/namespace.yaml | 8 + resources/dex/service-account.yaml | 9 + resources/dex/service.yaml | 15 + .../prometheus-role-binding.yaml | 0 .../prometheus-role.yaml | 0 src/api/che.ts | 44 +-- src/api/context.ts | 13 + src/api/kube.ts | 240 +++++++++---- src/commands/cacert/export.ts | 22 +- src/commands/server/deploy.ts | 60 ++-- src/constants.ts | 2 + src/tasks/che.ts | 19 +- .../component-installers/cert-manager.ts | 66 ++-- src/tasks/component-installers/dex.ts | 338 ++++++++++++++++++ src/tasks/installers/common-tasks.ts | 20 +- src/tasks/installers/helm.ts | 14 +- src/tasks/installers/olm.ts | 21 +- src/tasks/installers/operator.ts | 39 +- src/tasks/platforms/minikube.ts | 58 ++- src/tasks/platforms/platform.ts | 57 ++- src/util.ts | 63 +++- test/api/che.test.ts | 12 +- test/e2e/e2e.test.ts | 2 +- test/e2e/util.ts | 2 +- yarn.lock | 156 +++++++- 32 files changed, 1133 insertions(+), 287 deletions(-) rename {installers => resources}/cert-manager.yml (100%) create mode 100644 resources/dex/cluster-role-binding.yaml create mode 100644 resources/dex/cluster-role.yaml create mode 100644 resources/dex/configmap.yaml create mode 100644 resources/dex/deployment.yaml create mode 100644 resources/dex/ingress.yaml create mode 100644 resources/dex/namespace.yaml create mode 100644 resources/dex/service-account.yaml create mode 100644 resources/dex/service.yaml rename {installers => resources}/prometheus-role-binding.yaml (100%) rename {installers => resources}/prometheus-role.yaml (100%) create mode 100644 src/tasks/component-installers/dex.ts diff --git a/package.json b/package.json index 303768016..c000001fe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "analytics-node": "^5.1.0", "ansi-colors": "4.1.1", "axios": "^0.21.1", + "bcrypt": "^5.0.1", "cli-ux": "^5.6.3", "command-exists": "^1.2.9", "countries-and-timezones": "^3.2.3", @@ -52,6 +53,7 @@ "@eclipse-che/api": "latest", "@oclif/dev-cli": "^1", "@oclif/test": "^1", + "@types/bcrypt": "^5.0.0", "@types/chai": "^4", "@types/command-exists": "^1.2.0", "@types/countries-and-timezones": "^3.2.3", @@ -94,7 +96,7 @@ "files": [ "/bin", "/lib", - "/installers", + "/resources", "/npm-shrinkwrap.json", "/oclif.manifest.json", "/prepare-che-operator-templates.js" diff --git a/installers/cert-manager.yml b/resources/cert-manager.yml similarity index 100% rename from installers/cert-manager.yml rename to resources/cert-manager.yml diff --git a/resources/dex/cluster-role-binding.yaml b/resources/dex/cluster-role-binding.yaml new file mode 100644 index 000000000..cecd1e723 --- /dev/null +++ b/resources/dex/cluster-role-binding.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dex + labels: + app: dex +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dex +subjects: +- kind: ServiceAccount + name: dex + namespace: dex diff --git a/resources/dex/cluster-role.yaml b/resources/dex/cluster-role.yaml new file mode 100644 index 000000000..f956fb58c --- /dev/null +++ b/resources/dex/cluster-role.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dex + labels: + app: dex +rules: +- apiGroups: ["dex.coreos.com"] + resources: ["*"] + verbs: ["*"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["create"] diff --git a/resources/dex/configmap.yaml b/resources/dex/configmap.yaml new file mode 100644 index 000000000..6c6327b60 --- /dev/null +++ b/resources/dex/configmap.yaml @@ -0,0 +1,32 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: dex + namespace: dex + labels: + app: dex +data: + config.yaml: | + issuer: https://dex.{{DOMAIN}} + storage: + type: kubernetes + config: + inCluster: true + web: + http: 0.0.0.0:5556 + + oauth2: + skipApprovalScreen: true + + staticClients: + - id: {{CLIENT_ID}} + redirectURIs: + - 'https://che-{{NAMESPACE}}.{{DOMAIN}}/oauth2/callback' + name: 'Eclipse Che' + secret: {{CLIENT_SECRET}} + enablePasswordDB: true + staticPasswords: + - email: "che@eclipse.org" + hash: "{{DEX_PASSWORD_HASH}}" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/resources/dex/deployment.yaml b/resources/dex/deployment.yaml new file mode 100644 index 000000000..75020f996 --- /dev/null +++ b/resources/dex/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dex + namespace: dex + labels: + app: dex +spec: + replicas: 1 + selector: + matchLabels: + app: dex + template: + metadata: + labels: + app: dex + spec: + serviceAccountName: dex + containers: + - image: ghcr.io/dexidp/dex:v2.30.0 + name: dex + command: ["/usr/local/bin/dex", "serve", "/etc/dex/cfg/config.yaml"] + ports: + - name: https + containerPort: 5556 + volumeMounts: + - name: config + mountPath: /etc/dex/cfg + - name: tls + mountPath: /etc/dex/tls + env: + - name: KUBERNETES_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumes: + - name: config + configMap: + name: dex + items: + - key: config.yaml + path: config.yaml + - name: tls + secret: + secretName: dex.tls diff --git a/resources/dex/ingress.yaml b/resources/dex/ingress.yaml new file mode 100644 index 000000000..0f5383689 --- /dev/null +++ b/resources/dex/ingress.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dex + namespace: dex + labels: + app: dex + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + rules: + - host: dex.{{DOMAIN}} + http: + paths: + - backend: + service: + name: dex + port: + number: 5556 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - dex.{{DOMAIN}} + secretName: dex.tls + diff --git a/resources/dex/namespace.yaml b/resources/dex/namespace.yaml new file mode 100644 index 000000000..82cae0850 --- /dev/null +++ b/resources/dex/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: dex + labels: + app: dex + diff --git a/resources/dex/service-account.yaml b/resources/dex/service-account.yaml new file mode 100644 index 000000000..a5b894644 --- /dev/null +++ b/resources/dex/service-account.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dex + namespace: dex + labels: + app: dex + diff --git a/resources/dex/service.yaml b/resources/dex/service.yaml new file mode 100644 index 000000000..618522798 --- /dev/null +++ b/resources/dex/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: dex + namespace: dex + labels: + app: dex +spec: + ports: + - name: dex + port: 5556 + protocol: TCP + selector: + app: dex diff --git a/installers/prometheus-role-binding.yaml b/resources/prometheus-role-binding.yaml similarity index 100% rename from installers/prometheus-role-binding.yaml rename to resources/prometheus-role-binding.yaml diff --git a/installers/prometheus-role.yaml b/resources/prometheus-role.yaml similarity index 100% rename from installers/prometheus-role.yaml rename to resources/prometheus-role.yaml diff --git a/src/api/che.ts b/src/api/che.ts index 0fd27da5b..881ec8019 100644 --- a/src/api/che.ts +++ b/src/api/che.ts @@ -14,7 +14,6 @@ import { che as chetypes } from '@eclipse-che/api' import { CoreV1Api, V1Pod, Watch } from '@kubernetes/client-node' import axios, { AxiosInstance } from 'axios' import * as cp from 'child_process' -import { cli } from 'cli-ux' import * as commandExists from 'command-exists' import * as fs from 'fs-extra' import * as https from 'https' @@ -25,7 +24,7 @@ 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, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, OPERATOR_TEMPLATE_DIR } from '../constants' +import { CHE_ROOT_CA_SECRET_NAME, DEFAULT_CHE_OLM_PACKAGE_NAME, DEFAULT_OPENSHIFT_OPERATORS_NS_NAME, OPERATOR_TEMPLATE_DIR } from '../constants' import { base64Decode, downloadFile } from '../util' import { CheApiClient } from './che-api-client' import { Devfile } from './types/devfile' @@ -186,27 +185,6 @@ export class CheHelper { throw new Error(`Secret "${CHE_ROOT_CA_SECRET_NAME}" has invalid format: "ca.crt" key not found in data.`) } - async saveCheCaCert(cheCaCert: string, destination?: string): Promise { - const cheCaCertFile = this.getTargetFile(destination) - fs.writeFileSync(cheCaCertFile, cheCaCert) - return cheCaCertFile - } - - /** - * Handles certificate target location and returns string which points to the target file. - */ - private getTargetFile(destination: string | undefined): string { - if (!destination) { - return path.join(os.tmpdir(), DEFAULT_CA_CERT_FILE_NAME) - } - - if (fs.existsSync(destination)) { - return fs.lstatSync(destination).isDirectory() ? path.join(destination, DEFAULT_CA_CERT_FILE_NAME) : destination - } - - throw new Error(`Given path \'${destination}\' doesn't exist.`) - } - /** * Retrieves Keycloak admin user credentials. * Works only with installers which use Che CR (operator, olm). @@ -240,7 +218,7 @@ export class CheHelper { } async chePluginRegistryK8sURL(namespace = ''): Promise { - if (await this.kube.ingressExist('plugin-registry', namespace)) { + if (await this.kube.isIngressExist('plugin-registry', namespace)) { const protocol = await this.kube.getIngressProtocol('plugin-registry', namespace) const hostname = await this.kube.getIngressHost('plugin-registry', namespace) return `${protocol}://${hostname}` @@ -260,7 +238,7 @@ export class CheHelper { async cheK8sURL(namespace = ''): Promise { const ingress_names = ['che', 'che-ingress'] for (const ingress_name of ingress_names) { - if (await this.kube.ingressExist(ingress_name, namespace)) { + if (await this.kube.isIngressExist(ingress_name, namespace)) { const protocol = await this.kube.getIngressProtocol(ingress_name, namespace) const hostname = await this.kube.getIngressHost(ingress_name, namespace) return `${protocol}://${hostname}` @@ -404,22 +382,6 @@ export class CheHelper { () => { }) } - /** - * Wait until workspace is in 'Active` state. - */ - async waitNamespaceActive(namespaceName: string, intervalMs = 500, timeoutMs = 60000) { - const iterations = timeoutMs / intervalMs - for (let index = 0; index < iterations; index++) { - const namespace = await this.kube.getNamespace(namespaceName) - if (namespace && namespace.status && namespace.status.phase && namespace.status.phase === 'Active') { - return - } - await cli.wait(intervalMs) - } - - throw new Error(`ERR_TIMEOUT: ${namespaceName} is not 'Active'.`) - } - /** * Indicates if pod matches given labels. */ diff --git a/src/api/context.ts b/src/api/context.ts index bfc21f3e7..28bdc0a15 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -76,3 +76,16 @@ export namespace ChectlContext { return ctx } } + +export namespace OIDCContextKeys { + export const ISSUER_URL = 'oidc-issuer-url' + export const CLIENT_ID = 'oidc-client-id' + export const CA_FILE = 'oidc-ca-file' +} + +export namespace DexContextKeys { + export const DEX_CA_CRT = 'dex-ca.crt' + export const DEX_USERNAME = 'dex-username' + export const DEX_PASSWORD = 'dex-password' + export const DEX_PASSWORD_HASH = 'dex-password-hash' +} diff --git a/src/api/kube.ts b/src/api/kube.ts index 0be80eec6..4a392fe74 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -10,19 +10,19 @@ * Red Hat, Inc. - initial API and implementation */ -import { AdmissionregistrationV1Api, ApiextensionsV1Api, ApiextensionsV1beta1Api, ApisApi, AppsV1Api, AuthorizationV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, KubeConfig, Log, NetworkingV1Api, PortForward, RbacAuthorizationV1Api, V1ClusterRole, V1ClusterRoleBinding, V1ClusterRoleBindingList, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1ContainerStateTerminated, V1ContainerStateWaiting, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1IngressList, V1Job, V1JobSpec, V1LabelSelector, V1MutatingWebhookConfiguration, V1Namespace, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodCondition, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleBindingList, V1RoleList, V1RoleRef, V1Secret, V1SelfSubjectAccessReview, V1SelfSubjectAccessReviewSpec, V1Service, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' +import { AdmissionregistrationV1Api, ApiextensionsV1Api, ApiextensionsV1beta1Api, ApisApi, AppsV1Api, AuthorizationV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, KubeConfig, Log, NetworkingV1Api, PortForward, RbacAuthorizationV1Api, V1ClusterRole, V1ClusterRoleBinding, V1ClusterRoleBindingList, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1ContainerStateTerminated, V1ContainerStateWaiting, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Ingress, V1IngressList, V1Job, V1JobSpec, V1LabelSelector, V1MutatingWebhookConfiguration, V1Namespace, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodCondition, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleBindingList, V1RoleList, V1RoleRef, V1Secret, V1SelfSubjectAccessReview, V1SelfSubjectAccessReviewSpec, V1Service, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' import { Cluster, Context } from '@kubernetes/client-node/dist/config_types' import axios, { AxiosRequestConfig } from 'axios' import { cli } from 'cli-ux' import * as execa from 'execa' import * as fs from 'fs' -import https = require('https') +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_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, OLM_STABLE_CHANNEL_NAME } from '../constants' import { base64Encode, getClusterClientCommand, getImageNameAndTag, isKubernetesPlatformFamily, newError, safeLoadFromYamlFile } from '../util' +import { ChectlContext } from './context' import { V1CheClusterBackup, V1CheClusterRestore } from './types/backup-restore-crds' import { V1Certificate } from './types/cert-manager' @@ -52,7 +52,6 @@ export class KubeHelper { } async createNamespace(namespaceName: string, labels: any): Promise { - const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) const namespaceObject = { apiVersion: 'v1', kind: 'Namespace', @@ -62,13 +61,39 @@ export class KubeHelper { }, } + return this.createNamespaceFromObj(namespaceObject) + } + + async createNamespaceFromFile(filePath: string): Promise { + const namespace = this.safeLoadFromYamlFile(filePath) as V1Namespace + return this.createNamespaceFromObj(namespace) + } + + async createNamespaceFromObj(namespace: V1Namespace): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) try { - await k8sCoreApi.createNamespace(namespaceObject) + await k8sCoreApi.createNamespace(namespace) } catch (e) { throw this.wrapK8sClientError(e) } } + /** + * Wait until workspace is in 'Active` state. + */ + async waitNamespaceActive(name: string, intervalMs = 500, timeoutMs = 60000) { + const iterations = timeoutMs / intervalMs + for (let index = 0; index < iterations; index++) { + const namespace = await this.getNamespace(name) + if (namespace && namespace.status && namespace.status.phase && namespace.status.phase === 'Active') { + return + } + await cli.wait(intervalMs) + } + + throw new Error(`Namespace '${name}' is not in 'Active' phase.`) + } + async deleteAllServices(namespace: string): Promise { const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) try { @@ -120,13 +145,17 @@ export class KubeHelper { throw new Error(`ERR_TIMEOUT: Timeout set to waiting for service ${timeoutMs}`) } - async serviceAccountExist(name = '', namespace = ''): Promise { + async isServiceAccountExist(name: string, namespace: string): Promise { const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) try { - const { body } = await k8sApi.readNamespacedServiceAccount(name, namespace) - return this.compare(body, name) - } catch { - return false + await k8sApi.readNamespacedServiceAccount(name, namespace) + return true + } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) } } @@ -198,7 +227,7 @@ export class KubeHelper { } } - async createServiceAccountFromFile(filePath: string, namespace = '') { + async createServiceAccountFromFile(filePath: string, namespace: string) { const yamlServiceAccount = this.safeLoadFromYamlFile(filePath) as V1ServiceAccount const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) try { @@ -231,13 +260,17 @@ export class KubeHelper { } } - async clusterRoleExist(name = ''): Promise { + async isClusterRoleExist(name: string): Promise { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) try { - const { body } = await k8sRbacAuthApi.readClusterRole(name) - return this.compare(body, name) - } catch { - return false + await k8sRbacAuthApi.readClusterRole(name) + return true + } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) } } @@ -274,7 +307,7 @@ export class KubeHelper { } } - async createRoleFrom(yamlRole: V1Role, namespace: string) { + async createRoleFromObj(yamlRole: V1Role, namespace: string) { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) try { const res = await k8sRbacAuthApi.createNamespacedRole(namespace, yamlRole) @@ -286,10 +319,10 @@ export class KubeHelper { async createRoleFromFile(filePath: string, namespace: string) { const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role - return this.createRoleFrom(yamlRole, namespace) + return this.createRoleFromObj(yamlRole, namespace) } - async replaceRoleFrom(yamlRole: V1Role, namespace: string) { + async replaceRoleFromObj(yamlRole: V1Role, namespace: string) { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) if (!yamlRole.metadata || !yamlRole.metadata.name) { @@ -305,7 +338,7 @@ export class KubeHelper { async replaceRoleFromFile(filePath: string, namespace: string) { const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role - return this.replaceRoleFrom(yamlRole, namespace) + return this.replaceRoleFromObj(yamlRole, namespace) } async listClusterRoles(): Promise { @@ -318,7 +351,7 @@ export class KubeHelper { } } - async createClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { + async createClusterRoleFromObj(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) if (!yamlClusterRole.metadata) { yamlClusterRole.metadata = {} @@ -339,10 +372,10 @@ export class KubeHelper { async createClusterRoleFromFile(filePath: string, clusterRoleName?: string) { const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole - return this.createClusterRoleFrom(yamlClusterRole, clusterRoleName) + return this.createClusterRoleFromObj(yamlClusterRole, clusterRoleName) } - async replaceClusterRoleFrom(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { + async replaceClusterRoleFromObj(yamlClusterRole: V1ClusterRole, clusterRoleName?: string) { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) if (!yamlClusterRole.metadata) { yamlClusterRole.metadata = {} @@ -363,7 +396,7 @@ export class KubeHelper { async replaceClusterRoleFromFile(filePath: string, clusterRoleName?: string) { const yamlClusterRole = this.safeLoadFromYamlFile(filePath) as V1ClusterRole - return this.replaceClusterRoleFrom(yamlClusterRole, clusterRoleName) + return this.replaceClusterRoleFromObj(yamlClusterRole, clusterRoleName) } async addClusterRoleRule(name: string, apiGroups: string[], resources: string[], verbs: string[]): Promise { @@ -441,7 +474,7 @@ export class KubeHelper { await k8sRbacAuthApi.readNamespacedRoleBinding(name, namespace) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -455,7 +488,7 @@ export class KubeHelper { await k8sAdmissionApi.readMutatingWebhookConfiguration(name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -479,7 +512,7 @@ export class KubeHelper { await k8sAdmissionApi.readValidatingWebhookConfiguration(name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -519,13 +552,17 @@ export class KubeHelper { } } - async clusterRoleBindingExist(name: string): Promise { + async isClusterRoleBindingExist(name: string): Promise { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) try { - const { body } = await k8sRbacAuthApi.readClusterRoleBinding(name) - return this.compare(body, name) - } catch { - return false + await k8sRbacAuthApi.readClusterRoleBinding(name) + return true + } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) } } @@ -550,7 +587,7 @@ export class KubeHelper { } } - async createRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { + async createRoleBindingFromObj(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) try { const response = await k8sRbacAuthApi.createNamespacedRoleBinding(namespace, yamlRoleBinding) @@ -562,10 +599,10 @@ export class KubeHelper { async createRoleBindingFromFile(filePath: string, namespace: string): Promise { const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding - return this.createRoleBindingFrom(yamlRoleBinding, namespace) + return this.createRoleBindingFromObj(yamlRoleBinding, namespace) } - async replaceRoleBindingFrom(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { + async replaceRoleBindingFromObj(yamlRoleBinding: V1RoleBinding, namespace: string): Promise { if (!yamlRoleBinding.metadata || !yamlRoleBinding.metadata.name) { throw new Error('RoleBinding object requires name') } @@ -581,17 +618,22 @@ export class KubeHelper { async replaceRoleBindingFromFile(filePath: string, namespace: string): Promise { const yamlRoleBinding = this.safeLoadFromYamlFile(filePath) as V1RoleBinding - return this.replaceRoleBindingFrom(yamlRoleBinding, namespace) + return this.replaceRoleBindingFromObj(yamlRoleBinding, namespace) + } + + async createClusterRoleBindingRoleFromFile(filePath: string): Promise { + const clusterRoleBinding = this.safeLoadFromYamlFile(filePath) as V1ClusterRoleBinding + return this.createClusterRoleBindingFromObj(clusterRoleBinding) } - async createClusterRoleBindingFrom(yamlClusterRoleBinding: V1ClusterRoleBinding) { + async createClusterRoleBindingFromObj(yamlClusterRoleBinding: V1ClusterRoleBinding): Promise { if (!yamlClusterRoleBinding.metadata || !yamlClusterRoleBinding.metadata.name) { throw new Error('ClusterRoleBinding object requires name') } const k8sRbacAuthApi = this.kubeConfig.makeApiClient(RbacAuthorizationV1Api) try { - return await k8sRbacAuthApi.createClusterRoleBinding(yamlClusterRoleBinding) + await k8sRbacAuthApi.createClusterRoleBinding(yamlClusterRoleBinding) } catch (e) { throw this.wrapK8sClientError(e) } @@ -616,10 +658,10 @@ export class KubeHelper { apiGroup: 'rbac.authorization.k8s.io', }, } as V1ClusterRoleBinding - return this.createClusterRoleBindingFrom(clusterRoleBinding) + return this.createClusterRoleBindingFromObj(clusterRoleBinding) } - async replaceClusterRoleBindingFrom(clusterRoleBinding: V1ClusterRoleBinding) { + async replaceClusterRoleBindingFromObj(clusterRoleBinding: V1ClusterRoleBinding) { if (!clusterRoleBinding.metadata || !clusterRoleBinding.metadata.name) { throw new Error('Cluster Role Binding must have name specified') } @@ -651,7 +693,7 @@ export class KubeHelper { apiGroup: 'rbac.authorization.k8s.io', }, } as V1ClusterRoleBinding - return this.replaceClusterRoleBindingFrom(clusterRoleBinding) + return this.replaceClusterRoleBindingFromObj(clusterRoleBinding) } async deleteRoleBinding(name: string, namespace: string): Promise { @@ -698,7 +740,7 @@ export class KubeHelper { } } - async createConfigMapFromFile(filePath: string, namespace = '') { + async createConfigMapFromFile(filePath: string, namespace: string) { const yamlConfigMap = this.safeLoadFromYamlFile(filePath) as V1ConfigMap return this.createNamespacedConfigMap(namespace, yamlConfigMap) } @@ -949,7 +991,7 @@ export class KubeHelper { } } - async waitForPodReady(selector: string, namespace = '', intervalMs = 500, timeoutMs = this.podReadyTimeout) { + async waitForPodReady(selector: string, namespace: string, intervalMs = 500, timeoutMs = this.podReadyTimeout) { const iterations = timeoutMs / intervalMs for (let index = 0; index < iterations; index++) { const readyStatus = await this.getPodReadyConditionStatus(selector, namespace) @@ -1007,13 +1049,17 @@ export class KubeHelper { throw new Error(`ERR_TIMEOUT: Timeout set to pod wait timeout ${this.podWaitTimeout}`) } - async deploymentExist(name = '', namespace = ''): Promise { + async isDeploymentExist(name: string, namespace: string): Promise { const k8sApi = this.kubeConfig.makeApiClient(AppsV1Api) try { - const { body } = await k8sApi.readNamespacedDeployment(name, namespace) - return this.compare(body, name) - } catch { - return false + await k8sApi.readNamespacedDeployment(name, namespace) + return true + } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } + + throw this.wrapK8sClientError(e) } } @@ -1023,7 +1069,7 @@ export class KubeHelper { await k8sApi.readNamespacedConfigMap(name, namespace) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -1153,25 +1199,49 @@ export class KubeHelper { } } - async createDeploymentFrom(yamlDeployment: V1Deployment): Promise { + async createDeploymentFromFile(filePath: string, namespace: string): Promise { + const deployment = this.safeLoadFromYamlFile(filePath) as V1Deployment + return this.createDeploymentFromObj(deployment, namespace) + } + + async createDeploymentFromObj(yamlDeployment: V1Deployment, namespace: string): Promise { const k8sAppsApi = this.kubeConfig.makeApiClient(AppsV1Api) try { - await k8sAppsApi.createNamespacedDeployment(yamlDeployment.metadata!.namespace!, yamlDeployment) + await k8sAppsApi.createNamespacedDeployment(namespace, yamlDeployment) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async createServiceFromFile(filePath: string, namespace: string): Promise { + const service = this.safeLoadFromYamlFile(filePath) as V1Service + return this.createServiceFromObj(service, namespace) + } + + async isServiceExists(name: string, namespace: string): Promise { + const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) + try { + await k8sCoreApi.readNamespacedService(name, namespace) + return true } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } + throw this.wrapK8sClientError(e) } } - async createServiceFrom(yamlService: V1Service, namespace = '') { + async createServiceFromObj(yamlService: V1Service, namespace: string): Promise { const k8sApi = this.kubeConfig.makeApiClient(CoreV1Api) try { - return await k8sApi.createNamespacedService(namespace, yamlService) + await k8sApi.createNamespacedService(namespace, yamlService) } catch (e) { throw this.wrapK8sClientError(e) } } - async replaceDeploymentFrom(yamlDeployment: V1Deployment): Promise { + async replaceDeploymentFromObj(yamlDeployment: V1Deployment): Promise { // updating restartedAt to make sure that rollout will be restarted let annotations = yamlDeployment.spec!.template!.metadata!.annotations if (!annotations) { @@ -1388,12 +1458,29 @@ export class KubeHelper { } } - async ingressExist(name: string, namespace: string): Promise { + async createIngressFromFile(filePath: string, namespace: string) { + const yamlIngress = this.safeLoadFromYamlFile(filePath) as V1Ingress + return this.createIngressFromObj(yamlIngress, namespace) + } + + async createIngressFromObj(ingress: V1Ingress, namespace: string) { const networkingV1Api = this.kubeConfig.makeApiClient(NetworkingV1Api) try { - const { body } = await networkingV1Api.readNamespacedIngress(name, namespace) - return this.compare(body, name) - } catch { + return await networkingV1Api.createNamespacedIngress(namespace, ingress) + } catch (e) { + throw this.wrapK8sClientError(e) + } + } + + async isIngressExist(name: string, namespace: string): Promise { + const networkingV1Api = this.kubeConfig.makeApiClient(NetworkingV1Api) + try { + await networkingV1Api.readNamespacedIngress(name, namespace) + return true + } catch (e) { + if (e.response && e.response.statusCode === 404) { + return false + } return false } } @@ -1477,7 +1564,7 @@ export class KubeHelper { const { body } = await k8sApi.readCustomResourceDefinition(name) return body } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return } @@ -1491,7 +1578,7 @@ export class KubeHelper { const { body } = await k8sApi.readCustomResourceDefinition(name) return body } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return } @@ -1576,7 +1663,7 @@ export class KubeHelper { if (flags.tls) { cheClusterCR.spec.server.tlsSupport = flags.tls if (!cheClusterCR.spec.k8s.tlsSecretName) { - cheClusterCR.spec.k8s.tlsSecretName = 'che-tls' + cheClusterCR.spec.k8s.tlsSecretName = DEFAULT_CHE_TLS_SECRET_NAME } } if (flags.domain) { @@ -1608,8 +1695,8 @@ export class KubeHelper { cheClusterCR.spec.server.cheClusterRoles = ctx.namespaceEditorClusterRoleName // override default values - if (ctx.crPatch) { - merge(cheClusterCR, ctx.crPatch) + if (ctx[ChectlContext.CR_PATCH]) { + merge(cheClusterCR, ctx[ChectlContext.CR_PATCH]) } const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi) @@ -1697,7 +1784,7 @@ export class KubeHelper { const { body } = await customObjectsApi.listClusterCustomObject(resourceAPIGroup, resourceAPIVersion, resourcePlural) return (body as any).items ? (body as any).items : [] } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { // There is no CRD return [] } @@ -1728,7 +1815,7 @@ export class KubeHelper { await customObjectsApi.deleteNamespacedCustomObject(resourceAPIGroup, resourceAPIVersion, namespace, resourcePlural, cr.metadata.name) } } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { // There is no CRD return } @@ -1868,7 +1955,7 @@ export class KubeHelper { const oauthClientAuthorizations = (body as any).items as any[] return oauthClientAuthorizations.filter(o => o.clientName === clientName) } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { // There is no 'oauthclientauthorizations` return [] } @@ -1884,7 +1971,7 @@ export class KubeHelper { await customObjectsApi.deleteClusterCustomObject('oauth.openshift.io', 'v1', 'oauthclientauthorizations', oauthAuthorization.metadata.name) } } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return } throw this.wrapK8sClientError(e) @@ -1897,7 +1984,7 @@ export class KubeHelper { await customObjectsApi.getClusterCustomObject('console.openshift.io', 'v1', 'consolelinks', name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { // There are no consoleLink return false } @@ -1971,7 +2058,7 @@ export class KubeHelper { try { await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'catalogsources', catalogSourceName) } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return } throw this.wrapK8sClientError(e) @@ -2088,7 +2175,7 @@ export class KubeHelper { try { await customObjectsApi.deleteNamespacedCustomObject('operators.coreos.com', 'v1alpha1', namespace, 'subscriptions', operatorSubscriptionName) } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return } throw this.wrapK8sClientError(e) @@ -2251,7 +2338,7 @@ export class KubeHelper { await customObjectsApi.getClusterCustomObject('cert-manager.io', version, 'clusterissuers', name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -2267,7 +2354,7 @@ export class KubeHelper { await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'certificates', name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -2348,7 +2435,7 @@ export class KubeHelper { await customObjectsApi.getNamespacedCustomObject('cert-manager.io', version, namespace, 'issuers', name) return true } catch (e) { - if (e.response.statusCode === 404) { + if (e.response && e.response.statusCode === 404) { return false } @@ -2558,7 +2645,7 @@ export class KubeHelper { throw new Error('ERR_LIST_INGRESSES') } - async getSecret(name: string, namespace = 'default'): Promise { + async getSecret(name: string, namespace: string): Promise { const k8sCoreApi = this.kubeConfig.makeApiClient(CoreV1Api) // now get the matching secrets @@ -2822,6 +2909,9 @@ export class KubeHelper { */ private wrapK8sClientError(e: any): Error { if (e.response && e.response.body) { + if (e.response.body.message) { + return newError(e.response.body.message, e) + } return newError(e.response.body, e) } return e diff --git a/src/commands/cacert/export.ts b/src/commands/cacert/export.ts index d2fe02732..3924fc25e 100644 --- a/src/commands/cacert/export.ts +++ b/src/commands/cacert/export.ts @@ -12,7 +12,9 @@ import { Command, flags } from '@oclif/command' import { string } from '@oclif/parser/lib/flags' - +import * as fs from 'fs-extra' +import * as os from 'os' +import * as path from 'path' import { CheHelper } from '../../api/che' import { ChectlContext } from '../../api/context' import { KubeHelper } from '../../api/kube' @@ -58,7 +60,8 @@ export default class Export extends Command { try { const cheCaCert = await cheHelper.retrieveCheCaCert(flags.chenamespace) if (cheCaCert) { - const targetFile = await cheHelper.saveCheCaCert(cheCaCert, flags.destination) + const targetFile = this.getTargetFile(flags.destination) + fs.writeFileSync(targetFile, cheCaCert) this.log(`Eclipse Che self-signed CA certificate is exported to ${targetFile}`) } else { this.log('Self signed certificate secret not found. Is commonly trusted certificate used?') @@ -67,4 +70,19 @@ export default class Export extends Command { this.error(wrapCommandError(err)) } } + + /** + * Handles certificate target location and returns string which points to the target file. + */ + private getTargetFile(destination: string | undefined): string { + if (!destination) { + return path.join(os.tmpdir(), DEFAULT_CA_CERT_FILE_NAME) + } + + if (fs.existsSync(destination)) { + return fs.lstatSync(destination).isDirectory() ? path.join(destination, DEFAULT_CA_CERT_FILE_NAME) : destination + } + + throw new Error(`Given path \'${destination}\' doesn't exist.`) + } } diff --git a/src/commands/server/deploy.ts b/src/commands/server/deploy.ts index 7e1c12562..370cc1dee 100644 --- a/src/commands/server/deploy.ts +++ b/src/commands/server/deploy.ts @@ -21,11 +21,12 @@ import { batch, cheDeployment, cheDeployVersion, cheNamespace, cheOperatorCRPatc import { DEFAULT_ANALYTIC_HOOK_NAME, DEFAULT_CHE_NAMESPACE, DEFAULT_OLM_SUGGESTED_NAMESPACE, DOCS_LINK_INSTALL_RUNNING_CHE_LOCALLY, MIN_CHE_OPERATOR_INSTALLER_VERSION, MIN_HELM_INSTALLER_VERSION, MIN_OLM_INSTALLER_VERSION, OLM_STABLE_ALL_NAMESPACES_CHANNEL_NAME } from '../../constants' import { CheTasks } from '../../tasks/che' import { DevWorkspaceTasks } from '../../tasks/component-installers/devfile-workspace-operator-installer' -import { checkChectlAndCheVersionCompatibility, downloadTemplates, getPrintHighlightedMessagesTask, retrieveCheCaCertificateTask } from '../../tasks/installers/common-tasks' +import { DexTasks } from '../../tasks/component-installers/dex' +import { checkChectlAndCheVersionCompatibility, createNamespaceTask, downloadTemplates, getPrintHighlightedMessagesTask, 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 { askForChectlUpdateIfNeeded, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, getProjectName, isKubernetesPlatformFamily, isOpenshiftPlatformFamily, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' +import { askForChectlUpdateIfNeeded, getCommandSuccessMessage, getEmbeddedTemplatesDirectory, getProjectName, getTlsSupport, isDevWorkspaceEnabled, isKubernetesPlatformFamily, isNativeUserModeEnabled, isOpenshiftPlatformFamily, notifyCommandCompletedSuccessfully, wrapCommandError } from '../../util' export default class Deploy extends Command { static description = 'Deploy Eclipse Che server' @@ -203,7 +204,7 @@ export default class Deploy extends Command { } async setPlaformDefaults(flags: any, ctx: any): Promise { - flags.tls = await this.checkTlsMode(ctx) + flags.tls = getTlsSupport(ctx) if (flags['self-signed-cert']) { this.warn('"self-signed-cert" flag is deprecated and has no effect. Autodetection is used instead.') } @@ -229,38 +230,6 @@ export default class Deploy extends Command { } } - /** - * Checks if TLS is disabled via operator custom resource. - * Returns true if TLS is enabled (or omitted) and false if it is explicitly disabled. - */ - async checkTlsMode(ctx: any): Promise { - const crPatch = ctx.crPatch - if (crPatch && crPatch.spec && crPatch.spec.server && crPatch.spec.server.tlsSupport === false) { - return false - } - - const customCR = ctx.customCR - if (customCR && customCR.spec && customCR.spec.server && customCR.spec.server.tlsSupport === false) { - return false - } - - return true - } - - private isDevWorkspaceEnabled(ctx: any): boolean { - const crPatch = ctx.crPatch - if (crPatch && crPatch.spec && crPatch.spec.devWorkspace && crPatch.spec.devWorkspace.enable) { - return true - } - - const customCR = ctx.customCR - if (customCR && customCR.spec && customCR.spec.devWorkspace && customCR.spec.devWorkspace.enable) { - return true - } - - return false - } - private checkCompatibility(flags: any) { if (flags.installer === 'operator' && flags[CHE_OPERATOR_CR_YAML_KEY]) { const ignoredFlags = [] @@ -391,8 +360,9 @@ export default class Deploy extends Command { } } + const dexTasks = new DexTasks(flags) const cheTasks = new CheTasks(flags) - const platformTasks = new PlatformTasks() + const platformTasks = new PlatformTasks(flags) const installerTasks = new InstallerTasks() const apiTasks = new ApiTasks() const devWorkspaceTasks = new DevWorkspaceTasks(flags) @@ -411,10 +381,16 @@ export default class Deploy extends Command { preInstallTasks.add(downloadTemplates(flags)) preInstallTasks.add({ title: '🧪 DevWorkspace engine (experimental / technology preview) 🚨', - enabled: () => (this.isDevWorkspaceEnabled(ctx) || flags['workspace-engine'] === 'dev-workspace') && !ctx.isOpenShift, + enabled: () => (isDevWorkspaceEnabled(ctx) || flags['workspace-engine'] === 'dev-workspace') && !ctx.isOpenShift, task: () => new Listr(devWorkspaceTasks.getInstallTasks(flags)), }) - const installTasks = new Listr(await installerTasks.installTasks(flags, this), ctx.listrOptions) + + const installTasks = new Listr(undefined, ctx.listrOptions) + installTasks.add([createNamespaceTask(flags.chenamespace, this.getNamespaceLabels(flags))]) + if (isKubernetesPlatformFamily(flags.platform) && isNativeUserModeEnabled(ctx)) { + installTasks.add(dexTasks.getInstallTasks()) + } + installTasks.add(await installerTasks.installTasks(flags, this)) // Post Install Checks const postInstallTasks = new Listr([ @@ -458,6 +434,14 @@ export default class Deploy extends Command { } this.exit(0) } + + private getNamespaceLabels(flags: any): any { + // The label values must be strings + if (flags['cluster-monitoring'] && flags.platform === 'openshift') { + return { 'openshift.io/cluster-monitoring': 'true' } + } + return {} + } } /** diff --git a/src/constants.ts b/src/constants.ts index 3592f6962..cafad22ea 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -92,3 +92,5 @@ export const CHE_CLUSTER_BACKUP_CRD = 'checlusterbackups.org.eclipse.che' 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 DEFAULT_CHE_TLS_SECRET_NAME = 'che-tls' diff --git a/src/tasks/che.ts b/src/tasks/che.ts index 750f03be1..cc5902128 100644 --- a/src/tasks/che.ts +++ b/src/tasks/che.ts @@ -11,6 +11,7 @@ */ import { Command } from '@oclif/command' import * as Listr from 'listr' +import { DexContextKeys } from '../api/context' import { CheHelper } from '../api/che' import { CheApiClient } from '../api/che-api-client' @@ -147,7 +148,7 @@ export class CheTasks { { title: `Verify if Eclipse Che is deployed into namespace \"${this.cheNamespace}\"`, task: async (ctx: any, task: any) => { - if (await this.kube.deploymentExist(this.cheDeploymentName, this.cheNamespace)) { + if (await this.kube.isDeploymentExist(this.cheDeploymentName, this.cheNamespace)) { // helm chart and Eclipse Che operator use a deployment ctx.isCheDeployed = true ctx.isCheReady = await this.kube.deploymentReady(this.cheDeploymentName, this.cheNamespace) @@ -155,7 +156,7 @@ export class CheTasks { ctx.isCheStopped = await this.kube.deploymentStopped(this.cheDeploymentName, this.cheNamespace) } - ctx.isDashboardDeployed = await this.kube.deploymentExist(this.dashboardDeploymentName, this.cheNamespace) + ctx.isDashboardDeployed = await this.kube.isDeploymentExist(this.dashboardDeploymentName, this.cheNamespace) if (ctx.isDashboardDeployed) { ctx.isDashboardReady = await this.kube.deploymentReady(this.dashboardDeploymentName, this.cheNamespace) if (!ctx.isDashboardReady) { @@ -163,7 +164,7 @@ export class CheTasks { } } - ctx.isKeycloakDeployed = await this.kube.deploymentExist(this.keycloakDeploymentName, this.cheNamespace) + ctx.isKeycloakDeployed = await this.kube.isDeploymentExist(this.keycloakDeploymentName, this.cheNamespace) if (ctx.isKeycloakDeployed) { ctx.isKeycloakReady = await this.kube.deploymentReady(this.keycloakDeploymentName, this.cheNamespace) if (!ctx.isKeycloakReady) { @@ -171,7 +172,7 @@ export class CheTasks { } } - ctx.isPostgresDeployed = await this.kube.deploymentExist(this.postgresDeploymentName, this.cheNamespace) + ctx.isPostgresDeployed = await this.kube.isDeploymentExist(this.postgresDeploymentName, this.cheNamespace) if (ctx.isPostgresDeployed) { ctx.isPostgresReady = await this.kube.deploymentReady(this.postgresDeploymentName, this.cheNamespace) if (!ctx.isPostgresReady) { @@ -179,7 +180,7 @@ export class CheTasks { } } - ctx.isDevfileRegistryDeployed = await this.kube.deploymentExist(this.devfileRegistryDeploymentName, this.cheNamespace) + ctx.isDevfileRegistryDeployed = await this.kube.isDeploymentExist(this.devfileRegistryDeploymentName, this.cheNamespace) if (ctx.isDevfileRegistryDeployed) { ctx.isDevfileRegistryReady = await this.kube.deploymentReady(this.devfileRegistryDeploymentName, this.cheNamespace) if (!ctx.isDevfileRegistryReady) { @@ -187,7 +188,7 @@ export class CheTasks { } } - ctx.isPluginRegistryDeployed = await this.kube.deploymentExist(this.pluginRegistryDeploymentName, this.cheNamespace) + ctx.isPluginRegistryDeployed = await this.kube.isDeploymentExist(this.pluginRegistryDeploymentName, this.cheNamespace) if (ctx.isPluginRegistryDeployed) { ctx.isPluginRegistryReady = await this.kube.deploymentReady(this.pluginRegistryDeploymentName, this.cheNamespace) if (!ctx.isPluginRegistryReady) { @@ -758,6 +759,12 @@ export class CheTasks { messages.push(OUTPUT_SEPARATOR) } } + + if (ctx[DexContextKeys.DEX_USERNAME] && ctx[DexContextKeys.DEX_PASSWORD]) { + messages.push(`Dex admin credentials : ${ctx[DexContextKeys.DEX_USERNAME]}:${ctx[DexContextKeys.DEX_PASSWORD]}`) + messages.push(OUTPUT_SEPARATOR) + } + ctx.highlightedMessages = messages.concat(ctx.highlightedMessages) task.title = `${task.title}...done` }, diff --git a/src/tasks/component-installers/cert-manager.ts b/src/tasks/component-installers/cert-manager.ts index 11f9f7d9a..708c01bc5 100644 --- a/src/tasks/component-installers/cert-manager.ts +++ b/src/tasks/component-installers/cert-manager.ts @@ -11,14 +11,14 @@ */ import * as fs from 'fs-extra' +import * as os from 'os' import * as Listr from 'listr' import * as path from 'path' - import { CheHelper } from '../../api/che' import { KubeHelper } from '../../api/kube' import { V1Certificate } from '../../api/types/cert-manager' -import { CA_CERT_GENERATION_JOB_IMAGE, CERT_MANAGER_NAMESPACE_NAME, CHE_RELATED_COMPONENT_LABEL, CHE_ROOT_CA_SECRET_NAME, CHE_TLS_SECRET_NAME } from '../../constants' -import { base64Decode } from '../../util' +import { CA_CERT_GENERATION_JOB_IMAGE, CERT_MANAGER_NAMESPACE_NAME, CHE_RELATED_COMPONENT_LABEL, CHE_ROOT_CA_SECRET_NAME, CHE_TLS_SECRET_NAME, DEFAULT_CA_CERT_FILE_NAME } from '../../constants' +import { base64Decode, getEmbeddedTemplatesDirectory } from '../../util' import { getMessageImportCaCertIntoBrowser } from '../installers/common-tasks' export const CERT_MANAGER_CA_SECRET_NAME = 'ca' @@ -52,14 +52,14 @@ export class CertManagerTasks { }, }, { - title: 'Deploy cert-manager', + title: 'Deploy Cert Manager', enabled: ctx => !ctx.certManagerInstalled, task: async (ctx: any, task: any) => { 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') + yamlPath = path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', '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') @@ -69,7 +69,7 @@ export class CertManagerTasks { }, }, { - title: 'Wait for cert-manager', + title: 'Wait for Cert Manager', enabled: ctx => ctx.certManagerInstalled, task: async (ctx: any, task: any) => { if (!ctx.certManagerInstalled) { @@ -89,10 +89,7 @@ export class CertManagerTasks { ] } - /** - * Returns list of tasks which perform cert-manager checks and requests self-signed certificate for Che. - */ - getGenerateCertificatesTasks(flags: any): ReadonlyArray { + getGenerateCertManagerCACertificateTasks(flags: any): ReadonlyArray { return [ { title: 'Check Cert Manager CA certificate', @@ -148,6 +145,11 @@ export class CertManagerTasks { } }, }, + ] + } + + getCreateCertificateIssuerTasks(flags: any): ReadonlyArray { + return [ { title: 'Set up Eclipse Che certificates issuer', task: async (ctx: any, task: any) => { @@ -182,8 +184,18 @@ export class CertManagerTasks { } }, }, + ] + } + + getGenerateCertificatesTasks( + flags: any, + commonName: string, + dnsNames: string[], + secretName: string, + namespace: string): ReadonlyArray { + return [ { - title: 'Request certificate', + title: `Request certificate for dnsNames: [${dnsNames}]`, task: async (ctx: any, task: any) => { if (ctx.cheCertificateExists) { throw new Error('Eclipse Che certificate already exists.') @@ -193,18 +205,14 @@ export class CertManagerTasks { } const certificateTemplatePath = path.join(flags.templates, '/cert-manager/che-certificate.yml') - const certifiateYaml = this.kubeHelper.safeLoadFromYamlFile(certificateTemplatePath) as V1Certificate - - const CN = '*.' + flags.domain - certifiateYaml.spec.commonName = CN - certifiateYaml.spec.dnsNames = [flags.domain, CN] - if (ctx.clusterIssuersName) { - certifiateYaml.spec.issuerRef.name = ctx.clusterIssuersName - } + const certificate = this.kubeHelper.safeLoadFromYamlFile(certificateTemplatePath) as V1Certificate + certificate.metadata.namespace = namespace + certificate.spec.secretName = secretName + certificate.spec.commonName = commonName + certificate.spec.dnsNames = dnsNames + certificate.spec.issuerRef.name = ctx.clusterIssuersName - certifiateYaml.metadata.namespace = flags.chenamespace - - await this.kubeHelper.createCheClusterCertificate(certifiateYaml, ctx.certManagerK8sApiVersion) + await this.kubeHelper.createCheClusterCertificate(certificate, ctx.certManagerK8sApiVersion) ctx.cheCertificateExists = true task.title = `${task.title}...done` @@ -216,12 +224,15 @@ export class CertManagerTasks { if (ctx.clusterIssuersName === DEFAULT_CHE_CLUSTER_ISSUER_NAME) { task.title = 'Wait for self-signed certificate' } - - await this.kubeHelper.waitSecret(CHE_TLS_SECRET_NAME, flags.chenamespace, ['tls.key', 'tls.crt', 'ca.crt']) - + await this.kubeHelper.waitSecret(secretName, namespace, ['tls.key', 'tls.crt', 'ca.crt']) task.title = `${task.title}...ready` }, }, + ] + } + + getRetrieveCheCACertificate(flags: any): ReadonlyArray { + return [ { title: 'Retrieving Che CA certificate', task: async (ctx: any, task: any) => { @@ -232,14 +243,15 @@ export class CertManagerTasks { const cheSecret = await this.kubeHelper.getSecret(CHE_TLS_SECRET_NAME, flags.chenamespace) if (cheSecret && cheSecret.data) { const cheCaCrt = base64Decode(cheSecret.data['ca.crt']) - const cheCaCertPath = await this.cheHelper.saveCheCaCert(cheCaCrt) + const caCertFilePath = path.join(os.tmpdir(), DEFAULT_CA_CERT_FILE_NAME) + fs.writeFileSync(caCertFilePath, cheCaCrt) // We need to put self-signed CA certificate separately into CHE_ROOT_CA_SECRET_NAME secret await this.kubeHelper.createSecret(flags.chenamespace, CHE_ROOT_CA_SECRET_NAME, { 'ca.crt': cheCaCrt }) const serverStrategy = await this.kubeHelper.getConfigMapValue('che', flags.chenamespace, 'CHE_INFRA_KUBERNETES_SERVER__STRATEGY') if (serverStrategy !== 'single-host') { - ctx.highlightedMessages.push(getMessageImportCaCertIntoBrowser(cheCaCertPath)) + ctx.highlightedMessages.push(getMessageImportCaCertIntoBrowser(caCertFilePath)) } task.title = `${task.title}... done` } else { diff --git a/src/tasks/component-installers/dex.ts b/src/tasks/component-installers/dex.ts new file mode 100644 index 000000000..6ea3abfee --- /dev/null +++ b/src/tasks/component-installers/dex.ts @@ -0,0 +1,338 @@ +/** + * 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 { V1ConfigMap, V1Ingress, V1ObjectMeta } from '@kubernetes/client-node' +import * as bcrypt from 'bcrypt' +import { cli } from 'cli-ux' +import * as crypto from 'crypto' +import * as fs from 'fs-extra' +import * as yaml from 'js-yaml' +import * as Listr from 'listr' +import { merge } from 'lodash' +import * as os from 'os' +import * as path from 'path' +import { CheHelper } from '../../api/che' +import { ChectlContext, DexContextKeys, OIDCContextKeys } from '../../api/context' +import { KubeHelper } from '../../api/kube' +import { base64Decode, getEmbeddedTemplatesDirectory, getTlsSecretName } from '../../util' +import { PlatformTasks } from '../platforms/platform' +import { CertManagerTasks } from './cert-manager' + +namespace TemplatePlaceholders { + export const DOMAIN = '{{DOMAIN}}' + export const CHE_NAMESPACE = '{{NAMESPACE}}' + export const CLIENT_ID = '{{CLIENT_ID}}' + export const CLIENT_SECRET = '{{CLIENT_SECRET}}' + export const DEX_PASSWORD_HASH = '{{DEX_PASSWORD_HASH}}' +} + +namespace DexCaConfigMap { + export const NAME = 'dex-ca' + export const LABELS = { 'app.kubernetes.io/part-of': 'che.eclipse.org', 'app.kubernetes.io/component': 'ca-bundle' } +} + +export class DexTasks { + private static readonly DEX_USERNAME = 'admin' + + private static readonly DEX_PASSWORD = 'admin' + + private static readonly CLIENT_ID = 'eclipse-che' + + private static readonly DEX_NAME = 'dex' + + private static readonly NAMESPACE_NAME = 'dex' + + private static readonly TLS_SECRET_NAME = 'dex.tls' + + private static readonly CREDENTIALS_SECRET_NAME = 'dex-credentials' + + private static readonly CA_CERTIFICATE_FILENAME = 'dex-ca.crt' + + private static readonly SELECTOR = 'app=dex' + + protected kube: KubeHelper + + protected che: CheHelper + + protected platform: PlatformTasks + + constructor(private readonly flags: any) { + this.kube = new KubeHelper(flags) + this.che = new CheHelper(flags) + this.platform = new PlatformTasks(flags) + } + + getInstallTasks(): ReadonlyArray { + return [ + { + title: 'Deploy Dex', + task: async (ctx: any, _task: any) => { + return new Listr([ + { + title: `Create namespace: ${DexTasks.NAMESPACE_NAME}`, + task: async (_ctx: any, task: any) => { + if (await this.kube.getNamespace(DexTasks.NAMESPACE_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('namespace.yaml') + await this.kube.createNamespaceFromFile(yamlFilePath) + await this.kube.waitNamespaceActive(DexTasks.NAMESPACE_NAME) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Provide Dex certificate', + task: async (ctx: any) => { + const certs = new Listr(undefined, ctx.listrOptions) + + if (getTlsSecretName(ctx) === '') { + // Eclipse Che will use a default k8s certificate. + // No need to generate something for dex + certs.add([{ + title: 'Use default k8s certificate', + task: async (_ctx: any, task: any) => { + task.title = `${task.title}...[OK]` + }, + }]) + return certs + } + + if (!await this.kube.getSecret(DexTasks.TLS_SECRET_NAME, DexTasks.NAMESPACE_NAME)) { + const certManager = new CertManagerTasks(this.flags) + certs.add(certManager.getDeployCertManagerTasks(this.flags)) + certs.add(certManager.getGenerateCertManagerCACertificateTasks(this.flags)) + certs.add(certManager.getCreateCertificateIssuerTasks(this.flags)) + + const domain = 'dex.' + this.flags.domain + const commonName = '*.' + domain + const dnsNames = [domain, commonName] + certs.add(certManager.getGenerateCertificatesTasks(this.flags, commonName, dnsNames, DexTasks.TLS_SECRET_NAME, DexTasks.NAMESPACE_NAME)) + } + + certs.add([{ + title: 'Read Dex certificate', + task: async (ctx: any, task: any) => { + const secret = await this.kube.getSecret(DexTasks.TLS_SECRET_NAME, DexTasks.NAMESPACE_NAME) + if (secret && secret.data) { + ctx[DexContextKeys.DEX_CA_CRT] = base64Decode(secret.data['ca.crt']) + task.title = `${task.title}...[OK]` + } else { + throw new Error(`Dex certificate not found in the secret '${DexTasks.TLS_SECRET_NAME}' in the namespace '${DexTasks.NAMESPACE_NAME}'.`) + } + }, + }, + { + title: 'Save Dex certificate', + task: async (ctx: any, task: any) => { + const dexCaCertificateFilePath = this.getDexCaCertificateFilePath() + fs.writeFileSync(dexCaCertificateFilePath, ctx[DexContextKeys.DEX_CA_CRT]) + task.title = `${task.title}...[OK: ${dexCaCertificateFilePath}]` + }, + }, + { + title: 'Add Dex certificate to Eclipse Che certificates bundle', + task: async (ctx: any, task: any) => { + if (await this.kube.isConfigMapExists(DexCaConfigMap.NAME, this.flags.chenamespace)) { + task.title = `${task.title}...[Exists]` + } else { + const dexCa = new V1ConfigMap() + dexCa.metadata = new V1ObjectMeta() + dexCa.metadata.name = DexCaConfigMap.NAME + dexCa.metadata.labels = DexCaConfigMap.LABELS + dexCa.data = { 'ca.crt': ctx[DexContextKeys.DEX_CA_CRT] } + + await this.kube.createNamespacedConfigMap(this.flags.chenamespace, dexCa) + task.title = `${task.title}...[OK]` + } + }, + }]) + + return certs + }, + }, + { + title: 'Create Dex service account', + task: async (_ctx: any, task: any) => { + if (await this.kube.isServiceAccountExist(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('service-account.yaml') + await this.kube.createServiceAccountFromFile(yamlFilePath, DexTasks.NAMESPACE_NAME) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Create Dex cluster role', + task: async (_ctx: any, task: any) => { + if (await this.kube.isClusterRoleExist(DexTasks.DEX_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('cluster-role.yaml') + await this.kube.createClusterRoleFromFile(yamlFilePath) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Create Dex cluster role binding', + task: async (_ctx: any, task: any) => { + if (await this.kube.isClusterRoleBindingExist(DexTasks.DEX_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('cluster-role-binding.yaml') + await this.kube.createClusterRoleBindingRoleFromFile(yamlFilePath) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Create Dex service', + task: async (_ctx: any, task: any) => { + if (await this.kube.isServiceExists(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('service.yaml') + await this.kube.createServiceFromFile(yamlFilePath, DexTasks.NAMESPACE_NAME) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Create Dex ingress', + task: async (_ctx: any, task: any) => { + if (await this.kube.isIngressExist(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('ingress.yaml') + let yamlContent = fs.readFileSync(yamlFilePath).toString() + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.DOMAIN, 'g'), this.flags.domain) + + const ingress = yaml.load(yamlContent) as V1Ingress + await this.kube.createIngressFromObj(ingress, DexTasks.NAMESPACE_NAME) + + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Generate Dex username and password', + task: async (ctx: any, task: any) => { + const dexConfigMap = await this.kube.getConfigMap(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME) + if (dexConfigMap && dexConfigMap.data) { + task.title = `${task.title}...[Exists]` + } else { + // use the fixed password for now + const dexPassword = DexTasks.DEX_PASSWORD + + const salt = bcrypt.genSaltSync(10) + const dexPasswordHash = bcrypt.hashSync(dexPassword, salt) + + ctx[DexContextKeys.DEX_USERNAME] = DexTasks.DEX_USERNAME + ctx[DexContextKeys.DEX_PASSWORD] = dexPassword + ctx[DexContextKeys.DEX_PASSWORD_HASH] = dexPasswordHash + + // create a secret to store credentials + const credentials: any = { user: DexTasks.DEX_USERNAME, password: dexPassword} + await this.kube.createSecret(DexTasks.NAMESPACE_NAME, DexTasks.CREDENTIALS_SECRET_NAME, credentials) + + task.title = `${task.title}...[OK: ${ctx[DexContextKeys.DEX_USERNAME]}:${ctx[DexContextKeys.DEX_PASSWORD]}]` + } + }, + }, + { + title: 'Create Dex configmap', + task: async (ctx: any, task: any) => { + const dexConfigMap = await this.kube.getConfigMap(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME) + if (dexConfigMap && dexConfigMap.data) { + // read client secret + const configYamlData = dexConfigMap.data['config.yaml'] + if (!configYamlData) { + throw new Error(`'config.yaml' not defined in the configmap '${DexTasks.DEX_NAME}' in the namespace '${DexTasks.NAMESPACE_NAME}'`) + } + + const config = yaml.load(configYamlData) as any + const eclipseCheClient = (config.staticClients as Array).find(client => client.id === DexTasks.CLIENT_ID) + if (!eclipseCheClient) { + cli.error(`'${DexTasks.CLIENT_ID}' client not found in the configmap '${DexTasks.DEX_NAME}' in the namespace '${DexTasks.NAMESPACE_NAME}'.`) + } + + // set in a CR + ctx[ChectlContext.CR_PATCH] = ctx[ChectlContext.CR_PATCH] || {} + merge(ctx[ChectlContext.CR_PATCH], { spec: { auth: { oAuthClientName: DexTasks.CLIENT_ID, oAuthSecret: eclipseCheClient.secret } } }) + + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('configmap.yaml') + let yamlContent = fs.readFileSync(yamlFilePath).toString() + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.DOMAIN, 'g'), this.flags.domain) + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.CHE_NAMESPACE, 'g'), this.flags.chenamespace) + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.CLIENT_ID, 'g'), DexTasks.CLIENT_ID) + // generate client secret + const clientSecret = crypto.randomBytes(32).toString('base64') + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.CLIENT_SECRET, 'g'), clientSecret) + + yamlContent = yamlContent.replace(new RegExp(TemplatePlaceholders.DEX_PASSWORD_HASH, 'g'), ctx[DexContextKeys.DEX_PASSWORD_HASH]) + + const configMap = yaml.load(yamlContent) as V1ConfigMap + await this.kube.createNamespacedConfigMap(DexTasks.NAMESPACE_NAME, configMap) + + // set in a CR + merge(ctx[ChectlContext.CR_PATCH], { spec: { auth: { oAuthClientName: DexTasks.CLIENT_ID, oAuthSecret: clientSecret } } }) + + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Create Dex deployment', + task: async (_ctx: any, task: any) => { + if (await this.kube.isDeploymentExist(DexTasks.DEX_NAME, DexTasks.NAMESPACE_NAME)) { + task.title = `${task.title}...[Exists]` + } else { + const yamlFilePath = this.getDexResourceFilePath('deployment.yaml') + await this.kube.createDeploymentFromFile(yamlFilePath, DexTasks.NAMESPACE_NAME) + task.title = `${task.title}...[OK]` + } + }, + }, + { + title: 'Wait for Dex is ready', + task: async (_ctx: any, task: any) => { + await this.kube.waitForPodReady(DexTasks.SELECTOR, DexTasks.NAMESPACE_NAME) + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Configure API server', + task: async (ctx: any) => { + ctx[OIDCContextKeys.CLIENT_ID] = DexTasks.CLIENT_ID + ctx[OIDCContextKeys.ISSUER_URL] = `https://dex.${this.flags.domain}` + ctx[OIDCContextKeys.CA_FILE] = this.getDexCaCertificateFilePath() + return new Listr(this.platform.configureApiServerForDex(this.flags), ctx.listrOptions) + }, + }, + ], ctx.listrOptions) + }, + }, + ] + } + + getDexCaCertificateFilePath(): string { + return path.join(os.tmpdir(), DexTasks.CA_CERTIFICATE_FILENAME) + } + + getDexResourceFilePath(fileName: string): string { + return path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', 'dex', fileName) + } +} diff --git a/src/tasks/installers/common-tasks.ts b/src/tasks/installers/common-tasks.ts index 52c12f086..38cedaf3d 100644 --- a/src/tasks/installers/common-tasks.ts +++ b/src/tasks/installers/common-tasks.ts @@ -13,6 +13,7 @@ import Command from '@oclif/command' import ansi = require('ansi-colors') import * as fs from 'fs-extra' +import * as os from 'os' import * as Listr from 'listr' import { isEmpty } from 'lodash' import * as path from 'path' @@ -23,7 +24,7 @@ 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, OPERATOR_TEMPLATE_DIR } from '../../constants' +import { CHE_CLUSTER_CRD, DEFAULT_CA_CERT_FILE_NAME, DOCS_LINK_IMPORT_CA_CERT_INTO_BROWSER, OPERATOR_TEMPLATE_DIR } from '../../constants' import { getProjectVersion } from '../../util' export const TASK_TITLE_CREATE_CHE_CLUSTER_CRD = `Create the Custom Resource of type ${CHE_CLUSTER_CRD}` @@ -31,19 +32,18 @@ export const TASK_TITLE_PATCH_CHECLUSTER_CR = `Patching the Custom Resource of t export function createNamespaceTask(namespaceName: string, labels: {}): Listr.ListrTask { return { - title: `Create Namespace (${namespaceName})`, + title: `Create Namespace ${namespaceName}`, task: async (_ctx: any, task: any) => { const kube = new KubeHelper() - const che = new CheHelper({}) const namespace = await kube.getNamespace(namespaceName) if (namespace) { - await che.waitNamespaceActive(namespaceName) - task.title = `${task.title}...It already exists.` + await kube.waitNamespaceActive(namespaceName) + task.title = `${task.title}...[Exists]` } else { await kube.createNamespace(namespaceName, labels) - await che.waitNamespaceActive(namespaceName) - task.title = `${task.title}...Done.` + await kube.waitNamespaceActive(namespaceName) + task.title = `${task.title}...[OK]` } }, } @@ -189,12 +189,12 @@ export function retrieveCheCaCertificateTask(flags: any): Listr.ListrTask { const kube = new KubeHelper() const cheCaCert = await che.retrieveCheCaCert(flags.chenamespace) if (cheCaCert) { - const targetFile = await che.saveCheCaCert(cheCaCert) - + const caCertFilePath = path.join(os.tmpdir(), DEFAULT_CA_CERT_FILE_NAME) + fs.writeFileSync(caCertFilePath, cheCaCert) task.title = `${task.title}...OK` const serverStrategy = await kube.getConfigMapValue('che', flags.chenamespace, 'CHE_INFRA_KUBERNETES_SERVER__STRATEGY') if (serverStrategy !== 'single-host') { - ctx.highlightedMessages.push(getMessageImportCaCertIntoBrowser(targetFile)) + ctx.highlightedMessages.push(getMessageImportCaCertIntoBrowser(caCertFilePath)) } } else { task.title = `${task.title}... commonly trusted certificate is used.` diff --git a/src/tasks/installers/helm.ts b/src/tasks/installers/helm.ts index edac907b9..a70eb09a0 100644 --- a/src/tasks/installers/helm.ts +++ b/src/tasks/installers/helm.ts @@ -115,11 +115,17 @@ export class HelmTasks { task.title = `${task.title}...going to generate self-signed one` const certManagerTasks = new CertManagerTasks(flags) - const certManagerListTasks = new Listr(undefined, ctx.listrOptions) - certManagerListTasks.add(certManagerTasks.getDeployCertManagerTasks(flags)) - certManagerListTasks.add(certManagerTasks.getGenerateCertificatesTasks(flags)) + const certManagerTasksList = new Listr(undefined, ctx.listrOptions) + certManagerTasksList.add(certManagerTasks.getDeployCertManagerTasks(flags)) + certManagerTasksList.add(certManagerTasks.getGenerateCertManagerCACertificateTasks(flags)) + certManagerTasksList.add(certManagerTasks.getCreateCertificateIssuerTasks(flags)) - return certManagerListTasks + const commonName = '*.' + flags.domain + const dnsNames = [flags.domain, commonName] + certManagerTasksList.add(certManagerTasks.getGenerateCertificatesTasks(flags, commonName, dnsNames, CHE_TLS_SECRET_NAME, flags.chenamespace)) + certManagerTasksList.add(certManagerTasks.getRetrieveCheCACertificate(flags)) + + return certManagerTasksList } }, }, diff --git a/src/tasks/installers/olm.ts b/src/tasks/installers/olm.ts index 8309f0ad8..1240fd688 100644 --- a/src/tasks/installers/olm.ts +++ b/src/tasks/installers/olm.ts @@ -21,9 +21,9 @@ 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, OLM_STABLE_ALL_NAMESPACES_CHANNEL_NAME, DEFAULT_CHE_OPERATOR_SUBSCRIPTION_NAME } from '../../constants' -import { isKubernetesPlatformFamily } from '../../util' +import { getEmbeddedTemplatesDirectory, isKubernetesPlatformFamily } from '../../util' -import { createEclipseCheCluster, createNamespaceTask, patchingEclipseCheCluster } from './common-tasks' +import { createEclipseCheCluster, patchingEclipseCheCluster } from './common-tasks' 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' @@ -45,12 +45,11 @@ export class OLMTasks { const che = new CheHelper(flags) return [ this.isOlmPreInstalledTask(command, kube), - createNamespaceTask(flags.chenamespace, this.getOlmNamespaceLabels(flags)), { enabled: () => flags['cluster-monitoring'] && flags.platform === 'openshift', title: `Create Role ${this.prometheusRoleName} in namespace ${flags.chenamespace}`, task: async (_ctx: any, task: any) => { - const yamlFilePath = path.join(flags.templates, '..', 'installers', 'prometheus-role.yaml') + const yamlFilePath = path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', 'prometheus-role.yaml') const exist = await kube.roleExist(this.prometheusRoleName, flags.chenamespace) if (exist) { task.title = `${task.title}...It already exists.` @@ -65,7 +64,7 @@ export class OLMTasks { title: `Create RoleBinding ${this.prometheusRoleBindingName} in namespace ${flags.chenamespace}`, task: async (_ctx: any, task: any) => { const exist = await kube.roleBindingExist(this.prometheusRoleBindingName, flags.chenamespace) - const yamlFilePath = path.join(flags.templates, '..', 'installers', 'prometheus-role-binding.yaml') + const yamlFilePath = path.join(getEmbeddedTemplatesDirectory(), '..', 'resources', 'prometheus-role-binding.yaml') if (exist) { task.title = `${task.title}...It already exists.` @@ -133,8 +132,8 @@ export class OLMTasks { task: async (ctx: any, task: any) => { if (!await kube.catalogSourceExists(NEXT_CATALOG_SOURCE_NAME, flags.chenamespace)) { const catalogSourceImage = `quay.io/eclipse/eclipse-che-${ctx.generalPlatformName}-opm-catalog:preview` - const nigthlyCatalogSource = this.constructIndexCatalogSource(flags.chenamespace, catalogSourceImage) - await kube.createCatalogSource(nigthlyCatalogSource) + const nightlyCatalogSource = this.constructIndexCatalogSource(flags.chenamespace, catalogSourceImage) + await kube.createCatalogSource(nightlyCatalogSource) await kube.waitCatalogSource(flags.chenamespace, NEXT_CATALOG_SOURCE_NAME) } else { task.title = `${task.title}...It already exists.` @@ -488,12 +487,4 @@ export class OLMTasks { throw new Error(`Unable to retrieve Che cluster CR definition from CSV: ${currentCSV}`) } } - - private getOlmNamespaceLabels(flags: any): any { - // The label values must be strings - if (flags['cluster-monitoring'] && flags.platform === 'openshift') { - return { 'openshift.io/cluster-monitoring': 'true' } - } - return {} - } } diff --git a/src/tasks/installers/operator.ts b/src/tasks/installers/operator.ts index ad8e268c3..9a68af0b7 100644 --- a/src/tasks/installers/operator.ts +++ b/src/tasks/installers/operator.ts @@ -15,15 +15,13 @@ import { cli } from 'cli-ux' import * as fs from 'fs' 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_BACKUP_SERVER_CONFIG_CRD, CHE_CLUSTER_API_GROUP, CHE_CLUSTER_API_VERSION, CHE_CLUSTER_BACKUP_CRD, CHE_CLUSTER_CRD, CHE_CLUSTER_KIND_PLURAL, CHE_CLUSTER_RESTORE_CRD, CHE_OPERATOR_SELECTOR, OPERATOR_DEPLOYMENT_NAME, OPERATOR_TEMPLATE_DIR } from '../../constants' import { getImageNameAndTag, safeLoadFromYamlFile } from '../../util' import { KubeTasks } from '../kube' - -import { createEclipseCheCluster, createNamespaceTask, patchingEclipseCheCluster } from './common-tasks' +import { createEclipseCheCluster, patchingEclipseCheCluster } from './common-tasks' export class OperatorTasks { operatorServiceAccount = 'che-operator' @@ -94,20 +92,20 @@ export class OperatorTasks { 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) + await kube.replaceRoleFromObj(role, flags.chenamespace) } } else { - await kube.createRoleFrom(role, flags.chenamespace) + await kube.createRoleFromObj(role, flags.chenamespace) } } 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) + await kube.replaceRoleBindingFromObj(roleBinding, flags.chenamespace) } } else { - await kube.createRoleBindingFrom(roleBinding, flags.chenamespace) + await kube.createRoleBindingFromObj(roleBinding, flags.chenamespace) } } @@ -116,12 +114,12 @@ export class OperatorTasks { for (const clusterRole of ctx.clusterRoles as V1ClusterRole[]) { const clusterRoleName = clusterObjectNamePrefix + clusterRole.metadata!.name - if (await kube.clusterRoleExist(clusterRoleName)) { + if (await kube.isClusterRoleExist(clusterRoleName)) { if (shouldUpdate) { - await kube.replaceClusterRoleFrom(clusterRole, clusterRoleName) + await kube.replaceClusterRoleFromObj(clusterRole, clusterRoleName) } } else { - await kube.createClusterRoleFrom(clusterRole, clusterRoleName) + await kube.createClusterRoleFromObj(clusterRole, clusterRoleName) } } @@ -131,12 +129,12 @@ export class OperatorTasks { for (const subj of clusterRoleBinding.subjects || []) { subj.namespace = flags.chenamespace } - if (await kube.clusterRoleBindingExist(clusterRoleBinding.metadata!.name)) { + if (await kube.isClusterRoleBindingExist(clusterRoleBinding.metadata!.name)) { if (shouldUpdate) { - await kube.replaceClusterRoleBindingFrom(clusterRoleBinding) + await kube.replaceClusterRoleBindingFromObj(clusterRoleBinding) } } else { - await kube.createClusterRoleBindingFrom(clusterRoleBinding) + await kube.createClusterRoleBindingFromObj(clusterRoleBinding) } } @@ -157,11 +155,10 @@ export class OperatorTasks { command.warn('Consider using the more reliable \'OLM\' installer when deploying a stable release of Eclipse Che (--installer=olm).') } return [ - 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) + const exist = await kube.isServiceAccountExist(this.operatorServiceAccount, flags.chenamespace) if (exist) { task.title = `${task.title}...It already exists.` } else { @@ -235,13 +232,13 @@ export class OperatorTasks { { title: `Create 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 exist = await kube.isDeploymentExist(OPERATOR_DEPLOYMENT_NAME, flags.chenamespace) if (exist) { task.title = `${task.title}...It already exists.` } else { const deploymentPath = path.join(ctx.resourcesPath, 'operator.yaml') const operatorDeployment = await this.readOperatorDeployment(deploymentPath, flags) - await kube.createDeploymentFrom(operatorDeployment) + await kube.createDeploymentFromObj(operatorDeployment, flags.chenamespace) task.title = `${task.title}...done.` } }, @@ -318,7 +315,7 @@ export class OperatorTasks { { 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 exist = await kube.isServiceAccountExist(this.operatorServiceAccount, flags.chenamespace) const yamlFilePath = path.join(ctx.resourcesPath, 'service_account.yaml') if (exist) { await kube.replaceServiceAccountFromFile(yamlFilePath, flags.chenamespace) @@ -409,14 +406,14 @@ 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 exist = await kube.isDeploymentExist(OPERATOR_DEPLOYMENT_NAME, flags.chenamespace) const deploymentPath = path.join(ctx.resourcesPath, 'operator.yaml') const operatorDeployment = await this.readOperatorDeployment(deploymentPath, flags) if (exist) { - await kube.replaceDeploymentFrom(operatorDeployment) + await kube.replaceDeploymentFromObj(operatorDeployment) task.title = `${task.title}...updated.` } else { - await kube.createDeploymentFrom(operatorDeployment) + await kube.createDeploymentFromObj(operatorDeployment, flags.chenamespace) task.title = `${task.title}...created new one.` } }, diff --git a/src/tasks/platforms/minikube.ts b/src/tasks/platforms/minikube.ts index d357f5440..2f530fb15 100644 --- a/src/tasks/platforms/minikube.ts +++ b/src/tasks/platforms/minikube.ts @@ -14,10 +14,10 @@ import { Command } from '@oclif/command' import * as commandExists from 'command-exists' import * as execa from 'execa' import * as Listr from 'listr' - +import { OIDCContextKeys } from '../../api/context' import { KubeHelper } from '../../api/kube' import { VersionHelper } from '../../api/version' - +import { sleep } from '../../util' import { CommonPlatformTasks } from './common-platform-tasks' export class MinikubeTasks { @@ -86,7 +86,7 @@ export class MinikubeTasks { { title: 'Checking minikube version', task: async (ctx: any, task: any) => { - const version = await this.getMinikbeVersion() + const version = await this.getMinikubeVersion() const versionComponents = version.split('.') ctx.minikubeVersionMajor = parseInt(versionComponents[0], 10) ctx.minikubeVersionMinor = parseInt(versionComponents[1], 10) @@ -131,6 +131,55 @@ export class MinikubeTasks { ], { renderer: flags['listr-renderer'] as any }) } + configureApiServerForDex(flags: any): ReadonlyArray { + return [ + { + title: 'Copy Dex certificate into Minikube', + enabled: (ctx: any) => Boolean(ctx[OIDCContextKeys.CA_FILE]), + task: async (ctx: any, task: any) => { + const args: string[] = [] + args.push('cp') + args.push(ctx[OIDCContextKeys.CA_FILE]) + args.push('/etc/ca-certificates/dex-ca.crt') + + await execa('minikube', args, { timeout: 60000 }) + + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Configure Minikube API server', + task: async (ctx: any, task: any) => { + const args: string[] = [] + args.push(`--extra-config=apiserver.oidc-issuer-url=${ctx[OIDCContextKeys.ISSUER_URL]}`) + args.push(`--extra-config=apiserver.oidc-client-id=${ctx[OIDCContextKeys.CLIENT_ID]}`) + + if (ctx[OIDCContextKeys.CA_FILE]) { + args.push('--extra-config=apiserver.oidc-ca-file=/etc/ca-certificates/dex-ca.crt') + } + + args.push('--extra-config=apiserver.oidc-username-claim=email') + args.push('--extra-config=apiserver.oidc-groups-claim=groups') + args.push('start') + + await execa('minikube', args, { timeout: 60000 }) + + task.title = `${task.title}...[OK]` + }, + }, + { + title: 'Wait for Minikube API server', + task: async (_ctx: any, task: any) => { + const kube = new KubeHelper(flags) + await sleep(30 * 1000) + await kube.waitForPodReady('component=kube-apiserver', 'kube-system') + + task.title = `${task.title}...[OK]` + }, + }, + ] + } + async isMinikubeRunning(): Promise { const { exitCode } = await execa('minikube', ['status'], { timeout: 10000, reject: false }) if (exitCode === 0) { @@ -167,10 +216,11 @@ export class MinikubeTasks { return stdout } - async getMinikbeVersion(): Promise { + async getMinikubeVersion(): Promise { const { stdout } = await execa('minikube', ['version'], { timeout: 10000 }) const versionLine = stdout.split('\n')[0] const versionString = versionLine.trim().split(' ')[2].substr(1) return versionString } } + diff --git a/src/tasks/platforms/platform.ts b/src/tasks/platforms/platform.ts index da70567b8..07cc3bce3 100644 --- a/src/tasks/platforms/platform.ts +++ b/src/tasks/platforms/platform.ts @@ -10,8 +10,8 @@ * Red Hat, Inc. - initial API and implementation */ import Command from '@oclif/command' +import { cli } from 'cli-ux' import * as Listr from 'listr' - import { CRCHelper } from './crc' import { DockerDesktopTasks } from './docker-desktop' import { K8sTasks } from './k8s' @@ -22,18 +22,33 @@ import { OpenshiftTasks } from './openshift' /** * Platform specific tasks. - * - preflightCheck */ export class PlatformTasks { - preflightCheckTasks(flags: any, command: Command): ReadonlyArray { - const minikubeTasks = new MinikubeTasks() - const microk8sTasks = new MicroK8sTasks() - const minishiftTasks = new MinishiftTasks() - const openshiftTasks = new OpenshiftTasks() - const k8sTasks = new K8sTasks() - const crc = new CRCHelper() - const dockerDesktopTasks = new DockerDesktopTasks(flags) + protected minikubeTasks: MinikubeTasks + + protected microk8sTasks: MicroK8sTasks + + protected minishiftTasks: MinishiftTasks + + protected openshiftTasks: OpenshiftTasks + + protected k8sTasks: K8sTasks + + protected crc: CRCHelper + + protected dockerDesktopTasks: DockerDesktopTasks + constructor(flags: any) { + this.minikubeTasks = new MinikubeTasks() + this.microk8sTasks = new MicroK8sTasks() + this.minishiftTasks = new MinishiftTasks() + this.openshiftTasks = new OpenshiftTasks() + this.k8sTasks = new K8sTasks() + this.crc = new CRCHelper() + this.dockerDesktopTasks = new DockerDesktopTasks(flags) + } + + preflightCheckTasks(flags: any, command: Command): ReadonlyArray { let task: Listr.ListrTask if (!flags.platform) { task = { @@ -45,38 +60,38 @@ export class PlatformTasks { } else if (flags.platform === 'openshift') { task = { title: '✈️ Openshift preflight checklist', - task: () => openshiftTasks.preflightCheckTasks(flags, command), + task: () => this.openshiftTasks.preflightCheckTasks(flags, command), } } else if (flags.platform === 'crc') { task = { title: '✈️ CodeReady Containers preflight checklist', - task: () => crc.preflightCheckTasks(flags, command), + task: () => this.crc.preflightCheckTasks(flags, command), } // platform.ts BEGIN CHE ONLY } else if (flags.platform === 'minikube') { task = { title: '✈️ Minikube preflight checklist', - task: () => minikubeTasks.preflightCheckTasks(flags, command), + task: () => this.minikubeTasks.preflightCheckTasks(flags, command), } } else if (flags.platform === 'minishift') { task = { title: '✈️ Minishift preflight checklist', - task: () => minishiftTasks.preflightCheckTasks(flags, command), + task: () => this.minishiftTasks.preflightCheckTasks(flags, command), } } else if (flags.platform === 'microk8s') { task = { title: '✈️ MicroK8s preflight checklist', - task: () => microk8sTasks.preflightCheckTasks(flags, command), + task: () => this.microk8sTasks.preflightCheckTasks(flags, command), } } else if (flags.platform === 'k8s') { task = { title: '✈️ Kubernetes preflight checklist', - task: () => k8sTasks.preflightCheckTasks(flags, command), + task: () => this.k8sTasks.preflightCheckTasks(flags, command), } } else if (flags.platform === 'docker-desktop') { task = { title: '✈️ Docker Desktop preflight checklist', - task: () => dockerDesktopTasks.preflightCheckTasks(flags, command), + task: () => this.dockerDesktopTasks.preflightCheckTasks(flags, command), } // platform.ts END CHE ONLY } else { @@ -90,4 +105,12 @@ export class PlatformTasks { return [task] } + + configureApiServerForDex(flags: any): ReadonlyArray { + if (flags.platform === 'minikube') { + return this.minikubeTasks.configureApiServerForDex(flags) + } else { + cli.error(`It is not possible to configure API server for ${flags.platform}.`) + } + } } diff --git a/src/util.ts b/src/util.ts index 0925b17f7..8269c270f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -23,11 +23,10 @@ import * as os from 'os' import * as path from 'path' import * as readline from 'readline' import { promisify } from 'util' - import { ChectlContext } from './api/context' import { KubeHelper } from './api/kube' import { VersionHelper } from './api/version' -import { DEFAULT_CHE_NAMESPACE, LEGACY_CHE_NAMESPACE } from './constants' +import { DEFAULT_CHE_NAMESPACE, DEFAULT_CHE_TLS_SECRET_NAME, LEGACY_CHE_NAMESPACE } from './constants' const pkjson = require('../package.json') @@ -387,3 +386,63 @@ export function confirmYN(): Promise { process.stdin.on('keypress', keyPressHandler) }) } + +/** + * Checks if TLS is disabled via operator custom resource. + * Returns true if TLS is enabled (or omitted) and false if it is explicitly disabled. + */ +export function getTlsSupport(ctx: any): boolean { + const crPatch = ctx[ChectlContext.CR_PATCH] + if (crPatch && crPatch.spec && crPatch.spec.server && crPatch.spec.server.tlsSupport === false) { + return false + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.server && customCR.spec.server.tlsSupport === false) { + return false + } + + return true +} + +export function isDevWorkspaceEnabled(ctx: any): boolean { + const crPatch = ctx[ChectlContext.CR_PATCH] + if (crPatch && crPatch.spec && crPatch.spec.devWorkspace && crPatch.spec.devWorkspace.enable) { + return true + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.devWorkspace && customCR.spec.devWorkspace.enable) { + return true + } + + return false +} + +export function isNativeUserModeEnabled(ctx: any): boolean { + const crPatch = ctx[ChectlContext.CR_PATCH] + if (crPatch && crPatch.spec && crPatch.spec.auth && crPatch.spec.auth.nativeUserMode) { + return true + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.auth && customCR.spec.auth.nativeUserMode) { + return true + } + + return false +} + +export function getTlsSecretName(ctx: any): string { + const crPatch = ctx[ChectlContext.CR_PATCH] + if (crPatch && crPatch.spec && crPatch.spec.k8s && crPatch.spec.k8s.tlsSecretName) { + return crPatch.spec.k8s.tlsSecretName + } + + const customCR = ctx.customCR + if (customCR && customCR.spec && customCR.spec.k8s && customCR.spec.k8s.tlsSecretName) { + return customCR.spec.k8s.tlsSecretName + } + + return DEFAULT_CHE_TLS_SECRET_NAME +} diff --git a/test/api/che.test.ts b/test/api/che.test.ts index 78f474c97..ce3ff1c87 100644 --- a/test/api/che.test.ts +++ b/test/api/che.test.ts @@ -61,7 +61,7 @@ describe('Eclipse Che helper', () => { }) fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => true) + .stub(kube, 'isIngressExist', () => true) .stub(kube, 'isOpenShift', () => false) .stub(kube, 'getIngressProtocol', () => 'https') .stub(kube, 'getIngressHost', () => 'example.org') @@ -71,7 +71,7 @@ describe('Eclipse Che helper', () => { }) fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => false) + .stub(kube, 'isIngressExist', () => false) .stub(kube, 'isOpenShift', () => true) .stub(oc, 'routeExist', () => false) .do(() => ch.cheURL('che-namespace')) //ERR_ROUTE_NO_EXIST @@ -79,7 +79,7 @@ describe('Eclipse Che helper', () => { .it('fails fetching Eclipse Che URL when ingress does not exist') fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => false) + .stub(kube, 'isIngressExist', () => false) .stub(kube, 'isOpenShift', () => false) .do(() => ch.cheURL('che-namespace')) .catch(err => expect(err.message).to.match(/ERR_INGRESS_NO_EXIST/)) @@ -195,7 +195,7 @@ describe('Eclipse Che helper', () => { }) fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => true) + .stub(kube, 'isIngressExist', () => true) .stub(kube, 'isOpenShift', () => false) .stub(kube, 'getIngressProtocol', () => 'https') .stub(kube, 'getIngressHost', () => 'example.org') @@ -205,7 +205,7 @@ describe('Eclipse Che helper', () => { }) fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => false) + .stub(kube, 'isIngressExist', () => false) .stub(kube, 'isOpenShift', () => true) .stub(oc, 'routeExist', () => false) .do(() => ch.chePluginRegistryURL('che-namespace')) //ERR_ROUTE_NO_EXIST @@ -213,7 +213,7 @@ describe('Eclipse Che helper', () => { .it('fails fetching Plugin Registry URL when ingress does not exist') fancy .stub(kube, 'getNamespace', () => ({})) - .stub(kube, 'ingressExist', () => false) + .stub(kube, 'isIngressExist', () => false) .stub(kube, 'isOpenShift', () => false) .do(() => ch.chePluginRegistryURL('che-namespace')) .catch(err => expect(err.message).to.match(/ERR_INGRESS_NO_EXIST/)) diff --git a/test/e2e/e2e.test.ts b/test/e2e/e2e.test.ts index 56bc615e0..077696235 100644 --- a/test/e2e/e2e.test.ts +++ b/test/e2e/e2e.test.ts @@ -37,7 +37,7 @@ const INSTALLER_OPERATOR = 'operator' const INSTALLER_HELM = 'helm' const INSTALLER_OLM = 'olm' -const DEVFILE_URL = 'https://raw.githubusercontent.com/eclipse-che/che-devfile-registry/master/devfiles/go/devfile.yaml' +const DEVFILE_URL = 'https://raw.githubusercontent.com/eclipse-che/che-devfile-registry/main/devfiles/nodejs/devfile.yaml' function getDeployCommand(): string { const cheVersion = helper.getNewVersion() diff --git a/test/e2e/util.ts b/test/e2e/util.ts index a75fdc817..7bb0d4fc9 100644 --- a/test/e2e/util.ts +++ b/test/e2e/util.ts @@ -201,7 +201,7 @@ export class E2eHelper { // Return ingress and protocol from minikube platform async K8SHostname(ingressName: string, namespace: string): Promise { - if (await this.kubeHelper.ingressExist(ingressName, namespace)) { + if (await this.kubeHelper.isIngressExist(ingressName, namespace)) { const protocol = await this.kubeHelper.getIngressProtocol(ingressName, namespace) const hostname = await this.kubeHelper.getIngressHost(ingressName, namespace) diff --git a/yarn.lock b/yarn.lock index d0ece994d..36ca8724d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -555,6 +555,21 @@ underscore "^1.9.1" ws "^7.3.1" +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950" + integrity sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA== + dependencies: + detect-libc "^1.0.3" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.1" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.4" + tar "^6.1.0" + "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -973,6 +988,13 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bcrypt@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" + integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== + dependencies: + "@types/node" "*" + "@types/cacheable-request@^6.0.1": version "6.0.2" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" @@ -1451,6 +1473,11 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -1484,6 +1511,13 @@ acorn@^8.1.0, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + aggregate-error@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1622,6 +1656,19 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -1851,6 +1898,14 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bcrypt@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" + integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^3.1.0" + before-after-hook@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" @@ -2381,6 +2436,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + content-type@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -2503,6 +2563,13 @@ date-fns@^2.0.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2510,13 +2577,6 @@ debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2595,6 +2655,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" @@ -2605,6 +2670,11 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -2668,13 +2738,11 @@ ecc-jsbn@~0.1.1: "eclipse-che-operator@git://github.com/eclipse-che/che-operator#main": version "0.0.0" - uid ce230cde1834344b39b61e17dc09bea5031db261 - resolved "git://github.com/eclipse-che/che-operator#ce230cde1834344b39b61e17dc09bea5031db261" + resolved "git://github.com/eclipse-che/che-operator#c68fb1da6c9fda951c9000988f3821eebadfeab1" "eclipse-che-server@git://github.com/eclipse-che/che-server#main": version "0.0.0" - uid d217fd6ecf4b2dd3c8c2d352071d866d06bb359e - resolved "git://github.com/eclipse-che/che-server#d217fd6ecf4b2dd3c8c2d352071d866d06bb359e" + resolved "git://github.com/eclipse-che/che-server#63b33cabc088ee950c334a4cd6d17e5a7bfcd807" editorconfig@^0.15.0: version "0.15.3" @@ -3438,6 +3506,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + 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" @@ -3649,6 +3731,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -3745,6 +3832,14 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -5017,7 +5112,7 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -make-dir@^3.0.0: +make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -5295,6 +5390,11 @@ nock@^13.1.3: lodash.set "^4.3.2" propagate "^2.0.0" +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -5339,6 +5439,13 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -5390,6 +5497,16 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -5862,7 +5979,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.2, readable-stream@~2.3.6: +readable-stream@^2.0.2, readable-stream@^2.0.6, 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== @@ -6198,7 +6315,7 @@ 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== -set-blocking@^2.0.0: +set-blocking@^2.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" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -6474,7 +6591,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -6637,7 +6754,7 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^6.0.2: +tar@^6.0.2, tar@^6.1.0: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== @@ -7116,6 +7233,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"