From 1790beb06ae4d885aea1add57d2338772728ab2f Mon Sep 17 00:00:00 2001 From: Igor Vinokur Date: Thu, 19 Dec 2024 14:16:00 +0200 Subject: [PATCH] Fix the bug when kubeconfig file content is collapsed to one line after workspace restart (#1279) Change the parse format from JSON to YAML as the default format of the oc and kubectl tools is YAML. Do not substring new line when fetching the kubeconfig file content. --- .../__tests__/fixtures/kubeconfig.yaml | 20 +++++++++ .../services/__tests__/kubeConfigApi.spec.ts | 32 ++++++++++++--- .../services/helpers/exec.ts | 3 +- .../services/kubeConfigApi.ts | 41 +++++++++---------- 4 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 packages/dashboard-backend/src/devworkspaceClient/services/__tests__/fixtures/kubeconfig.yaml diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/fixtures/kubeconfig.yaml b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/fixtures/kubeconfig.yaml new file mode 100644 index 000000000..82c81f531 --- /dev/null +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/fixtures/kubeconfig.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Config +clusters: + - name: inCluster + cluster: + server: https://0.0.0.0:443 + certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + insecure-skip-tls-verify: false +users: + - name: developer + user: + token: token +contexts: + - name: logged-user + context: + user: developer + cluster: inCluster + name: logged-user +preferences: {} +current-context: logged-user diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/kubeConfigApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/kubeConfigApi.spec.ts index 19a950b3e..044fb8a77 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/kubeConfigApi.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/kubeConfigApi.spec.ts @@ -14,6 +14,8 @@ import * as mockClient from '@kubernetes/client-node'; import { CoreV1Api, V1PodList } from '@kubernetes/client-node'; +import fs from 'fs'; +import { parse, stringify } from 'yaml'; import * as helper from '@/devworkspaceClient/services/helpers/exec'; import { KubeConfigApiService } from '@/devworkspaceClient/services/kubeConfigApi'; @@ -28,7 +30,7 @@ const mockExecPrintenvHome = jest.fn().mockReturnValue({ stdError: '', }); -const mockExecCatKubeConfig = jest.fn().mockReturnValue({ +let mockExecCatKubeConfig = jest.fn().mockReturnValue({ stdOut: '', stdError: '', }); @@ -61,11 +63,14 @@ const spyExec = jest const namespace = 'user-che'; const workspaceName = 'workspace-1'; const containerName = 'container-1'; -const config = JSON.stringify({ +const config = { apiVersion: 'v1', kind: 'Config', 'current-context': 'logged-user', -}); + contexts: [], + clusters: [], + users: [], +}; describe('Kubernetes Config API Service', () => { let kubeConfigService: KubeConfigApiService; @@ -81,7 +86,7 @@ describe('Kubernetes Config API Service', () => { }, } as CoreV1Api; }); - kubeConfig.exportConfig = jest.fn().mockReturnValue(config); + kubeConfig.exportConfig = jest.fn().mockReturnValue(JSON.stringify(config)); kubeConfig.getCurrentCluster = jest.fn().mockReturnValue(''); kubeConfig.applyToRequest = jest.fn(); @@ -142,7 +147,24 @@ describe('Kubernetes Config API Service', () => { workspaceName, namespace, containerName, - ['sh', '-c', `echo '${config}' > ${kubeConfigDir}/config`], + ['sh', '-c', `echo '${stringify(config)}' > ${kubeConfigDir}/config`], + expect.anything(), + ); + }); + test('should merge configs', async () => { + const configContent = fs.readFileSync(__dirname + '/fixtures/kubeconfig.yaml', 'utf-8'); + mockExecCatKubeConfig = jest.fn().mockReturnValue({ + stdOut: configContent, + stdError: '', + }); + await kubeConfigService.injectKubeConfig(namespace, 'wksp-id'); + + expect(spyExec).toHaveBeenNthCalledWith( + 5, + workspaceName, + namespace, + containerName, + ['sh', '-c', `echo '${configContent}' > ${kubeConfigDir}/config`], expect.anything(), ); }); diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/exec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/exec.ts index ff49fcef1..c2bf3dbae 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/exec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/exec.ts @@ -104,8 +104,7 @@ export async function exec( return; } - let message = Buffer.from(event.data.substr(1), 'base64').toString('utf-8'); - message = message.replace(/\n/g, ' ').trim(); + const message = Buffer.from(event.data.substr(1), 'base64').toString('utf-8').trim(); if (channel === CHANNELS[CHANNELS.STD_OUT]) { stdOut += message; diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/kubeConfigApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/kubeConfigApi.ts index a3f772643..e1b52ecc8 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/kubeConfigApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/kubeConfigApi.ts @@ -12,6 +12,7 @@ import { helpers } from '@eclipse-che/common'; import * as k8s from '@kubernetes/client-node'; +import { parse, stringify } from 'yaml'; import { exec, ServerConfig } from '@/devworkspaceClient/services/helpers/exec'; import { @@ -215,11 +216,11 @@ export class KubeConfigApiService implements IKubeConfigApi { private setNamespaceInContext(kubeConfig: string, namespace: string): string { try { - const kubeConfigJson = JSON.parse(kubeConfig); - for (const context of kubeConfigJson.contexts) { + const kubeConfigYaml = parse(kubeConfig); + for (const context of kubeConfigYaml.contexts) { context.context.namespace = namespace; } - return JSON.stringify(kubeConfigJson, undefined, ' '); + return stringify(kubeConfigYaml); } catch (e) { logger.error(e, 'Failed to parse kubeconfig'); return kubeConfig; @@ -230,31 +231,27 @@ export class KubeConfigApiService implements IKubeConfigApi { // If the inbounds kubeconfig match the kubeconfig format then merge them private mergeKubeConfig(kubeconfigSource: string, generatedKubeconfig: string): string { try { - const kubeConfigJson = JSON.parse(kubeconfigSource); - const generatedKubeConfigJson = JSON.parse(generatedKubeconfig); - for (const context of generatedKubeConfigJson.contexts) { - if (kubeConfigJson.contexts.find((c: any) => c.name === context.name)) { - kubeConfigJson.contexts = kubeConfigJson.contexts.filter( - (c: any) => c.name !== context.name, - ); + const kubeConfig = parse(kubeconfigSource); + const generatedKubeConfig = parse(generatedKubeconfig); + for (const context of generatedKubeConfig.contexts) { + if (kubeConfig.contexts.find((c: any) => c.name === context.name)) { + kubeConfig.contexts = kubeConfig.contexts.filter((c: any) => c.name !== context.name); } - kubeConfigJson.contexts.push(context); + kubeConfig.contexts.push(context); } - for (const cluster of generatedKubeConfigJson.clusters) { - if (kubeConfigJson.clusters.find((c: any) => c.name === cluster.name)) { - kubeConfigJson.clusters = kubeConfigJson.clusters.filter( - (c: any) => c.name !== cluster.name, - ); + for (const cluster of generatedKubeConfig.clusters) { + if (kubeConfig.clusters.find((c: any) => c.name === cluster.name)) { + kubeConfig.clusters = kubeConfig.clusters.filter((c: any) => c.name !== cluster.name); } - kubeConfigJson.clusters.push(cluster); + kubeConfig.clusters.push(cluster); } - for (const user of generatedKubeConfigJson.users) { - if (kubeConfigJson.users.find((c: any) => c.name === user.name)) { - kubeConfigJson.users = kubeConfigJson.users.filter((c: any) => c.name !== user.name); + for (const user of generatedKubeConfig.users) { + if (kubeConfig.users.find((c: any) => c.name === user.name)) { + kubeConfig.users = kubeConfig.users.filter((c: any) => c.name !== user.name); } - kubeConfigJson.users.push(user); + kubeConfig.users.push(user); } - return JSON.stringify(kubeConfigJson, undefined, ' '); + return stringify(kubeConfig); } catch (e) { logger.error(e, 'Failed to merge kubeconfig, returning source'); return kubeconfigSource;