Skip to content
This repository has been archived by the owner on Apr 4, 2023. It is now read-only.

Commit

Permalink
chore: store gitconfig in a configmap
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig committed Feb 17, 2022
1 parent e1c6b10 commit 793e873
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 2 deletions.
4 changes: 3 additions & 1 deletion extensions/eclipse-che-theia-git-provisioner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"@theia/preferences": "next",
"ini": "^1.3.5",
"nsfw": "^2.1.2",
"@eclipse-che/theia-user-preferences-synchronizer": "0.0.1"
"@eclipse-che/theia-user-preferences-synchronizer": "0.0.1",
"@kubernetes/client-node": "^0.12.1",
"@eclipse-che/theia-remote-impl-che-server": "0.0.1"
},
"devDependencies": {
"@types/ini": "^1.3.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

import * as fs from 'fs-extra';
// eslint-disable-next-line spaced-comment
import * as ini from 'ini';
import * as k8s from '@kubernetes/client-node';
import * as nsfw from 'nsfw';

import { CheGitClient, CheGitService, GIT_USER_EMAIL, GIT_USER_NAME } from '../common/git-protocol';
import { Disposable, Emitter } from '@theia/core';
import { createFile, pathExists, readFile, writeFile } from 'fs-extra';
import { inject, injectable } from 'inversify';

import { CheK8SService } from '@eclipse-che/theia-remote-api/lib/common/k8s-service';
import { CheK8SServiceImpl } from '@eclipse-che/theia-remote-impl-che-server/lib/node/che-server-k8s-service-impl';
import { CheTheiaUserPreferencesSynchronizer } from '@eclipse-che/theia-user-preferences-synchronizer/lib/node/che-theia-preferences-synchronizer';
import { Disposable } from '@theia/core';
import { WorkspaceService } from '@eclipse-che/theia-remote-api/lib/common/workspace-service';
import { homedir } from 'os';
import { resolve } from 'path';

export const GIT_USER_CONFIG_PATH = resolve(homedir(), '.gitconfig');
export const GIT_GLOBAL_CONFIG_PATH = '/etc/gitconfig';
export const GITCONFIG_CONFIGMAP_NAME = 'workspace-git-config';

export interface UserConfiguration {
name: string | undefined;
Expand All @@ -36,6 +42,20 @@ export interface GitConfiguration {

@injectable()
export class GitConfigurationController implements CheGitService {
constructor(
@inject(WorkspaceService)
private readonly workspaceService: WorkspaceService,
@inject(CheK8SService)
private readonly cheK8SService: CheK8SServiceImpl
) {
this.createGitconfigConfigmapIfNeeded().then(exists => {
if (exists) {
this.updateUserGitconfigFromConfigmap();
}
});
this.onUserGitconfigChangedEvent(() => this.createGitconfigConfigmapIfNeededAndUpdate());
}

@inject(CheTheiaUserPreferencesSynchronizer)
protected preferencesService: CheTheiaUserPreferencesSynchronizer;

Expand All @@ -45,6 +65,88 @@ export class GitConfigurationController implements CheGitService {

protected client: CheGitClient;

private onUserGitconfigChangedEmitter = new Emitter();
private onUserGitconfigChangedEvent = this.onUserGitconfigChangedEmitter.event;

/**
* Returns true if the configmap already exists, otherwise returns false.
*/
private async createGitconfigConfigmapIfNeeded(): Promise<boolean> {
if (await this.isGitconfigConfigmapExists()) {
return true;
}
const configmap: k8s.V1ConfigMap = {
metadata: {
name: GITCONFIG_CONFIGMAP_NAME,
labels: {
'controller.devfile.io/mount-to-devworkspace': 'true',
'controller.devfile.io/watch-configmap': 'true',
},
annotations: {
'controller.devfile.io/mount-as': 'subpath',
'controller.devfile.io/mount-path': '/etc/',
},
},
data: { gitconfig: fs.existsSync(GIT_USER_CONFIG_PATH) ? fs.readFileSync(GIT_USER_CONFIG_PATH).toString() : '' },
};
try {
await this.cheK8SService
.makeApiClient(k8s.CoreV1Api)
.createNamespacedConfigMap(await this.workspaceService.getCurrentNamespace(), configmap);
} catch (e) {
console.error('Failed to create gitconfig configmap. ' + e);
}
return false;
}

private async isGitconfigConfigmapExists(): Promise<boolean> {
try {
const request = await this.cheK8SService
.makeApiClient(k8s.CoreV1Api)
.listNamespacedConfigMap(await this.workspaceService.getCurrentNamespace());
return (
request.body.items.find(
configmap => configmap.metadata && configmap.metadata.name === GITCONFIG_CONFIGMAP_NAME
) !== undefined
);
} catch (e) {
console.error('Failed to list configmaps. ' + e);
}
return false;
}

private async createGitconfigConfigmapIfNeededAndUpdate(): Promise<void> {
await this.createGitconfigConfigmapIfNeeded();
const client = this.cheK8SService.makeApiClient(k8s.CoreV1Api);
client.defaultHeaders = {
'Content-Type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH,
};
try {
await client.patchNamespacedConfigMap(
GITCONFIG_CONFIGMAP_NAME,
await this.workspaceService.getCurrentNamespace(),
{
data: { gitconfig: fs.readFileSync(GIT_USER_CONFIG_PATH).toString() },
}
);
} catch (e) {
console.error('Failed to update gitconfig configmap. ' + e);
}
}

private async updateUserGitconfigFromConfigmap(): Promise<void> {
try {
const request = await this.cheK8SService
.makeApiClient(k8s.CoreV1Api)
.readNamespacedConfigMap(GITCONFIG_CONFIGMAP_NAME, await this.workspaceService.getCurrentNamespace());
const content = request.body.data!.gitconfig;
fs.ensureFileSync(GIT_USER_CONFIG_PATH);
fs.writeFileSync(GIT_USER_CONFIG_PATH, content);
} catch (e) {
console.error('Failed to read gitconfig configmap. ' + e);
}
}

public async watchGitConfigChanges(): Promise<void> {
if (this.gitConfigWatcher) {
return;
Expand All @@ -58,13 +160,17 @@ export class GitConfigurationController implements CheGitService {
this.gitConfigWatcher = await nsfw(GIT_USER_CONFIG_PATH, async (events: nsfw.FileChangeEvent[]) => {
for (const event of events) {
if (event.action === nsfw.actions.MODIFIED) {
this.onUserGitconfigChangedEmitter.fire(undefined);

const userConfig = await this.getUserConfigurationFromGitConfig();
const preferences = await this.preferencesService.getPreferences();

(preferences as { [index: string]: string })[GIT_USER_NAME] = userConfig.name!;
(preferences as { [index: string]: string })[GIT_USER_EMAIL] = userConfig.email!;

await this.preferencesService.setPreferences(preferences);
} else if (event.action === nsfw.actions.CREATED) {
this.onUserGitconfigChangedEmitter.fire(undefined);
}
}
});
Expand Down Expand Up @@ -143,6 +249,7 @@ export class GitConfigurationController implements CheGitService {
await this.gitConfigWatcher.stop();
}
await writeFile(GIT_USER_CONFIG_PATH, ini.stringify(gitConfig));
this.onUserGitconfigChangedEmitter.fire(undefined);
if (this.gitConfigWatcher) {
await this.gitConfigWatcher.start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'reflect-metadata';
import * as fs from 'fs-extra';
import * as path from 'path';

import { CheK8SService, WorkspaceService } from '@eclipse-che/theia-remote-api/lib/common';
import {
GIT_USER_CONFIG_PATH,
GitConfigurationController,
Expand All @@ -32,14 +33,27 @@ describe('Test GitConfigurationController', () => {
getPreferences: cheTheiaUserPreferencesSynchronizerGetpreferencesMock,
setPreferences: cheTheiaUserPreferencesSynchronizerSetpreferencesMock,
} as any;
const workspaceService = {
getCurrentNamespace: jest.fn(),
} as any;
const coreV1ApiMock = {
listNamespacedConfigMap: jest.fn(),
};
const k8sServiceMakeApiClientMethod = jest.fn();
const k8sServiceMock = {
makeApiClient: k8sServiceMakeApiClientMethod,
} as any;

beforeEach(async () => {
jest.restoreAllMocks();
jest.resetAllMocks();
container = new Container();
container.bind(CheTheiaUserPreferencesSynchronizer).toConstantValue(cheTheiaUserPreferencesSynchronizer);
container.bind(WorkspaceService).toConstantValue(workspaceService);
container.bind(CheK8SService).toConstantValue(k8sServiceMock);
container.bind(GitConfigurationController).toSelf().inSingletonScope();
gitConfigurationController = container.get(GitConfigurationController);
k8sServiceMakeApiClientMethod.mockReturnValue(coreV1ApiMock);
});

test('check Update', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/

import * as fs from 'fs-extra';
import * as k8s from '@kubernetes/client-node';
import * as os from 'os';
import * as path from 'path';

import { CheK8SService, K8SRawResponse } from '@eclipse-che/theia-remote-api/lib/common/k8s-service';

Expand All @@ -22,8 +25,29 @@ export class K8SServiceImpl implements CheK8SService {
private kc: k8s.KubeConfig;

constructor() {
const kubeconfig: k8s.KubeConfig = JSON.parse(
fs.readFileSync(path.resolve(os.homedir(), '.kube', 'config')).toString()
);
const tokenPath = path.resolve(os.homedir(), '.kube', 'token');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const k8sUser: any = kubeconfig.users.find(user => user.name === 'developer');
if (k8sUser) {
fs.ensureFileSync(tokenPath);
fs.writeFileSync(tokenPath, k8sUser.user.token);
}
const user = {
name: 'inClusterUser',
authProvider: {
name: 'tokenFile',
config: {
tokenFile: tokenPath,
},
},
};
this.kc = new k8s.KubeConfig();
this.kc.loadFromCluster();
this.kc.users = [];
this.kc.addUser(user);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down

0 comments on commit 793e873

Please sign in to comment.