diff --git a/src/api/kube.ts b/src/api/kube.ts index e91286989..f7a28f642 100644 --- a/src/api/kube.ts +++ b/src/api/kube.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 **********************************************************************/ -import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' +import { ApiextensionsV1beta1Api, ApisApi, AppsV1Api, BatchV1Api, CoreV1Api, CustomObjectsApi, ExtensionsV1beta1Api, KubeConfig, Log, PortForward, RbacAuthorizationV1Api, V1beta1CustomResourceDefinition, V1beta1IngressList, V1ClusterRole, V1ClusterRoleBinding, V1ConfigMap, V1ConfigMapEnvSource, V1Container, V1DeleteOptions, V1Deployment, V1DeploymentList, V1DeploymentSpec, V1EnvFromSource, V1Job, V1JobSpec, V1LabelSelector, V1NamespaceList, V1ObjectMeta, V1PersistentVolumeClaimList, V1Pod, V1PodList, V1PodSpec, V1PodTemplateSpec, V1PolicyRule, V1Role, V1RoleBinding, V1RoleRef, V1Secret, V1ServiceAccount, V1ServiceList, V1Subject, Watch } from '@kubernetes/client-node' import { Cluster, Context } from '@kubernetes/client-node/dist/config_types' import axios, { AxiosRequestConfig } from 'axios' import { cli } from 'cli-ux' @@ -225,6 +225,16 @@ export class KubeHelper { } } + async getClusterRole(name: string): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) + try { + const { body } = await k8sRbacAuthApi.readClusterRole(name) + return body + } catch { + return + } + } + async createRoleFromFile(filePath: string, namespace = '') { const yamlRole = this.safeLoadFromYamlFile(filePath) as V1Role const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) @@ -292,6 +302,30 @@ export class KubeHelper { } } + async addClusterRoleRule(name: string, apiGroups: string[], resources: string[], verbs: string[]): Promise { + const k8sRbacAuthApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) + const clusterRole = await this.getClusterRole(name) + if (clusterRole) { + // Clean up metadata, otherwise replace role call will fail + clusterRole.metadata = {} + clusterRole.metadata.name = name + + // Add new policy + const additionaRule = new V1PolicyRule() + additionaRule.apiGroups = apiGroups + additionaRule.resources = resources + additionaRule.verbs = verbs + clusterRole.rules.push(additionaRule) + + try { + const { body } = await k8sRbacAuthApi.replaceClusterRole(name, clusterRole) + return body + } catch { + return + } + } + } + async deleteRole(name = '', namespace = '') { const k8sCoreApi = KubeHelper.KUBE_CONFIG.makeApiClient(RbacAuthorizationV1Api) try { @@ -518,6 +552,26 @@ export class KubeHelper { } } + async patchNamespacedPod(name: string, namespace: string, patch: any): Promise { + const k8sCoreApi = KubeHelper.KUBE_CONFIG.makeApiClient(CoreV1Api) + + // It is required to patch content-type, otherwise request will be rejected with 415 (Unsupported media type) error. + const requestOptions = { + headers: { + 'content-type': 'application/strategic-merge-patch+json' + } + } + + try { + const res = await k8sCoreApi.patchNamespacedPod(name, namespace, patch, undefined, undefined, requestOptions) + if (res && res.body) { + return res.body + } + } catch { + return + } + } + async podsExistBySelector(selector: string, namespace = ''): Promise { const k8sCoreApi = KubeHelper.KUBE_CONFIG.makeApiClient(CoreV1Api) let res diff --git a/src/tasks/platforms/minikube.ts b/src/tasks/platforms/minikube.ts index fa37864eb..f753416d6 100644 --- a/src/tasks/platforms/minikube.ts +++ b/src/tasks/platforms/minikube.ts @@ -13,6 +13,7 @@ import * as commandExists from 'command-exists' import * as execa from 'execa' import * as Listr from 'listr' +import { KubeHelper } from '../../api/kube' import { VersionHelper } from '../../api/version' import { CommonPlatformTasks } from './common-platform-tasks' @@ -22,6 +23,7 @@ export class MinikubeTasks { * Returns tasks list which perform preflight platform checks. */ preflightCheckTasks(flags: any, command: Command): Listr { + const kube = new KubeHelper(flags) return new Listr([ { title: 'Verify if kubectl is installed', @@ -79,6 +81,50 @@ export class MinikubeTasks { task.title = `${task.title}...${flags.domain}.` } }, + { + title: 'Checking minikube version', + task: async (ctx: any, task: any) => { + const version = await this.getMinikbeVersion() + const versionComponents = version.split('.') + ctx.minikubeVersionMajor = parseInt(versionComponents[0], 10) + ctx.minikubeVersionMinor = parseInt(versionComponents[1], 10) + ctx.minikubeVersionPatch = parseInt(versionComponents[2], 10) + + task.title = `${task.title}... ${version}` + } + }, + { + // Starting from Minikube 1.9 there is a bug with storage provisioner which prevents Che from successful deployment. + // For more details see https://github.com/kubernetes/minikube/issues/7218 + // To workaround the bug, it is required to patch storage provisioner as well as its permissions. + title: 'Patch minikube storage', + enabled: ctx => ctx.minikubeVersionMajor && ctx.minikubeVersionMinor && + ctx.minikubeVersionMajor === 1 && ctx.minikubeVersionMinor >= 9, + task: async (_ctx: any, task: any) => { + // Patch storage provisioner pod to the latest version + const storageProvisionerImage = 'gcr.io/k8s-minikube/storage-provisioner@sha256:bb22ad560924f0f111eb30ffc2dc1315736ab09979c5e77ff9d7d3737f671ca0' + const storageProvisionerImagePatch = { + apiVersion: 'v1', + kind: 'Pod', + spec: { + containers: [ + { name: 'storage-provisioner', image: storageProvisionerImage } + ] + } + } + if (! await kube.patchNamespacedPod('storage-provisioner', 'kube-system', storageProvisionerImagePatch)) { + throw new Error('Failed to patch storage provisioner image') + } + + // Set required permissions for cluster role of persistent volume provisioner + if (! await kube.addClusterRoleRule('system:persistent-volume-provisioner', + [''], ['endpoints'], ['get', 'list', 'watch', 'create', 'patch', 'update'])) { + throw new Error('Failed to patch permissions for persistent-volume-provisioner') + } + + task.title = `${task.title}... done` + } + }, CommonPlatformTasks.getPingClusterTask(flags) ], { renderer: flags['listr-renderer'] as any }) } @@ -106,4 +152,11 @@ export class MinikubeTasks { return stdout } + async getMinikbeVersion(): 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 + } + }