Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] "chore: Mount kubeconfig into users containers" #953

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ export { helpers, api };

export const FACTORY_LINK_ATTR = 'factoryLink';
export const ERROR_CODE_ATTR = 'error_code';
export const KUBECONFIG_MOUNT_PATH = '/tmp/.kube';

const common = {
helpers,
5 changes: 1 addition & 4 deletions packages/dashboard-backend/src/constants/schemas.ts
Original file line number Diff line number Diff line change
@@ -29,11 +29,8 @@ export const namespacedKubeConfigSchema: JSONSchema7 = {
namespace: {
type: 'string',
},
devworkspaceId: {
type: 'string',
},
},
required: ['namespace', 'devworkspaceId'],
required: ['namespace'],
};

export const namespacedWorkspaceSchema: JSONSchema7 = {
Original file line number Diff line number Diff line change
@@ -13,137 +13,67 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

import * as mockClient from '@kubernetes/client-node';
import { CoreV1Api, V1PodList } from '@kubernetes/client-node';
import { CoreV1Api, HttpError } from '@kubernetes/client-node';
import { IncomingMessage } from 'http';

import * as helper from '@/devworkspaceClient/services/helpers/exec';
import { KubeConfigApiService } from '@/devworkspaceClient/services/kubeConfigApi';

const homeUserDir = '/home/user';
const kubeConfigDir = `${homeUserDir}/.kube`;
const mockExecPrintenvHome = jest.fn().mockReturnValue({
stdOut: homeUserDir,
stdError: '',
});
const spyExec = jest
.spyOn(helper, 'exec')
.mockImplementation((...args: Parameters<typeof helper.exec>) => {
const [, , , command] = args;
if (command.some(c => c === 'printenv HOME')) {
// directory where to create the kubeconfig
return mockExecPrintenvHome();
} else if (command.some(c => c.startsWith('mkdir -p'))) {
// crete the directory
return Promise.resolve();
} else if (command.some(c => c.startsWith(`[ -f ${homeUserDir}`))) {
// sync config
return Promise.resolve();
}
return Promise.reject({
stdOut: '',
stdError: 'command executing error',
});
});

const namespace = 'user-che';
const workspaceName = 'workspace-1';
const containerName = 'container-1';
const config = JSON.stringify({
apiVersion: 'v1',
kind: 'Config',
'current-context': 'logged-user',
});

describe('Kubernetes Config API Service', () => {
let kubeConfigService: KubeConfigApiService;
let spyCreateNamespacedSecret: jest.SpyInstance;
let spyReadNamespacedSecret: jest.SpyInstance;
let spyReplaceNamespacedSecret: jest.SpyInstance;

console.error = jest.fn();
console.warn = jest.fn();

function initMocks(readNamespacedSecretReturnPromise: Promise<any>): void {
const stubCoreV1Api = {
createNamespacedSecret: (_namespace: string, secret: any) => {
return Promise.resolve({ body: secret });
},
readNamespacedSecret: () => {
return readNamespacedSecretReturnPromise;
},
replaceNamespacedSecret: (_name: string, _namespace: string, secret: any) => {
return Promise.resolve({ body: secret });
},
} as unknown as CoreV1Api;

spyCreateNamespacedSecret = jest.spyOn(stubCoreV1Api, 'createNamespacedSecret');
spyReadNamespacedSecret = jest.spyOn(stubCoreV1Api, 'readNamespacedSecret');
spyReplaceNamespacedSecret = jest.spyOn(stubCoreV1Api, 'replaceNamespacedSecret');

beforeEach(() => {
const { KubeConfig } = mockClient;
const kubeConfig = new KubeConfig();

kubeConfig.makeApiClient = jest.fn().mockImplementation(_api => {
return {
listNamespacedPod: namespace => {
return Promise.resolve(buildListNamespacedPod());
},
} as CoreV1Api;
});
kubeConfig.exportConfig = jest.fn().mockReturnValue(config);
kubeConfig.getCurrentCluster = jest.fn().mockReturnValue('');
kubeConfig.applyToRequest = jest.fn();
kubeConfig.makeApiClient = jest.fn().mockImplementation(_api => stubCoreV1Api);

kubeConfigService = new KubeConfigApiService(kubeConfig);
});
}

afterEach(() => {
jest.clearAllMocks();
});

test('injecting kubeconfig', async () => {
// mute output
console.error = jest.fn();
console.warn = jest.fn();

await kubeConfigService.injectKubeConfig(namespace, 'wksp-id');
expect(spyExec).toHaveBeenCalledTimes(4);

// should attempt to resolve the KUBECONFIG env variable
expect(spyExec).toHaveBeenNthCalledWith(
1,
workspaceName,
namespace,
containerName,
['sh', '-c', 'printenv KUBECONFIG'],
expect.anything(),
);

// should attempt to resolve the HOME env variable
expect(spyExec).toHaveBeenNthCalledWith(
2,
workspaceName,
namespace,
containerName,
['sh', '-c', 'printenv HOME'],
expect.anything(),
);

// should create the directory
expect(spyExec).toHaveBeenNthCalledWith(
3,
workspaceName,
namespace,
containerName,
['sh', '-c', `mkdir -p ${kubeConfigDir}`],
expect.anything(),
);

// should sync the kubeconfig to the container
expect(spyExec).toHaveBeenNthCalledWith(
4,
workspaceName,
namespace,
containerName,
['sh', '-c', `[ -f ${kubeConfigDir}/config ] || echo '${config}' > ${kubeConfigDir}/config`],
expect.anything(),
);
test('create kubeconfig Secret', async () => {
initMocks(Promise.reject(new HttpError({} as IncomingMessage, undefined, 404)));

await kubeConfigService.applyKubeConfigSecret(namespace);

expect(spyReadNamespacedSecret).toBeCalled();
expect(spyCreateNamespacedSecret).toBeCalled();
expect(spyReplaceNamespacedSecret).toBeCalledTimes(0);
});
});

function buildListNamespacedPod(): { body: V1PodList } {
return {
body: {
apiVersion: 'v1',
items: [
{
metadata: {
name: workspaceName,
namespace,
},
spec: {
containers: [{ name: containerName }],
},
},
],
kind: 'PodList',
},
};
}
test('replace kubeconfig Secret', async () => {
initMocks(Promise.resolve({} as any));

await kubeConfigService.applyKubeConfigSecret(namespace);

expect(spyReadNamespacedSecret).toBeCalled();
expect(spyCreateNamespacedSecret).toBeCalledTimes(0);
expect(spyReplaceNamespacedSecret).toBeCalled();
});
});
Loading