diff --git a/.rebase/CHANGELOG.md b/.rebase/CHANGELOG.md index 282efd1610cf..42697c78e196 100644 --- a/.rebase/CHANGELOG.md +++ b/.rebase/CHANGELOG.md @@ -560,3 +560,11 @@ https://github.com/che-incubator/che-code/pull/695 - code/build/npm/preinstall.ts --- + +#### @RomanNikitenko +https://github.com/che-incubator/che-code/pull/754 + +- code/src/vs/server/node/webClientServer.ts +- code/src/vs/workbench/browser/web.api.ts +- code/src/vs/workbench/browser/web.main.ts +--- diff --git a/.rebase/replace/code/src/vs/server/node/webClientServer.ts.json b/.rebase/replace/code/src/vs/server/node/webClientServer.ts.json index 1c86eda536cd..8fd1afcd044a 100644 --- a/.rebase/replace/code/src/vs/server/node/webClientServer.ts.json +++ b/.rebase/replace/code/src/vs/server/node/webClientServer.ts.json @@ -1,10 +1,14 @@ [ { "from": "import type * as http from 'http';", - "by": "import * as http from 'http';\nimport { getCheRedirectLocation } from './che/webClientServer.js';" + "by": "import * as http from 'http';\nimport { getCheInitialKeybindings, getCheRedirectLocation } from './che/webClientServer.js';" }, { "from": "\t\t\tconst newLocation = url.format({ pathname: basePath, query: newQuery });\n\t\t\tresponseHeaders['Location'] = newLocation;", "by": "\t\t\tresponseHeaders['Location'] = getCheRedirectLocation(req, newQuery);" + }, + { + "from": "callbackRoute: callbackRoute\n\t\t};", + "by": "callbackRoute: callbackRoute,\n\t\t\tcheInitialKeybindings: await getCheInitialKeybindings(),\n\t\t};" } ] diff --git a/.rebase/replace/code/src/vs/workbench/browser/web.api.ts.json b/.rebase/replace/code/src/vs/workbench/browser/web.api.ts.json new file mode 100644 index 000000000000..ad6a0d7f6737 --- /dev/null +++ b/.rebase/replace/code/src/vs/workbench/browser/web.api.ts.json @@ -0,0 +1,6 @@ +[ + { + "from": "readonly configurationDefaults?: Record;\n\n\t//#endregion\n\n\t//#region Profile options", + "by": "readonly configurationDefaults?: Record;\n\n\t//#endregion\n\n\t/**\n\t * Initial keybindings content from ConfigMap (JSON array string).\n\t * When set, keybindings are merged into IndexedDB on every workspace start.\n\t */\n\treadonly cheInitialKeybindings?: string;\n\n\t//#region Profile options" + } +] diff --git a/.rebase/replace/code/src/vs/workbench/browser/web.main.ts.json b/.rebase/replace/code/src/vs/workbench/browser/web.main.ts.json index e1bfe808940a..086dbb22a6d5 100644 --- a/.rebase/replace/code/src/vs/workbench/browser/web.main.ts.json +++ b/.rebase/replace/code/src/vs/workbench/browser/web.main.ts.json @@ -1,10 +1,14 @@ [ { "from": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';", - "by": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';\nimport { getPolicyService } from './che/web.js';" + "by": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';\nimport { CheKeybindingsInitializer, getPolicyService } from './che/web.js';" }, { "from": "const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService);", "by": "const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, getPolicyService(remoteAgentService,logService, this.configuration.remoteAuthority));" + }, + { + "from": "const userDataInitializationService = new UserDataInitializationService(userDataInitializers);", + "by": "if (environmentService.options.cheInitialKeybindings) {\n\t\t\tuserDataInitializers.push(new CheKeybindingsInitializer(environmentService.options.cheInitialKeybindings, fileService, userDataProfilesService, logService));\n\t\t}\n\t\tconst userDataInitializationService = new UserDataInitializationService(userDataInitializers);" } ] diff --git a/code/src/vs/server/node/che/webClientServer.ts b/code/src/vs/server/node/che/webClientServer.ts index 76f88da87ef3..d72228d91bf8 100644 --- a/code/src/vs/server/node/che/webClientServer.ts +++ b/code/src/vs/server/node/che/webClientServer.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2021-2022 Red Hat, Inc. + * Copyright (c) 2021-2026 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -9,9 +9,24 @@ ***********************************************************************/ /* eslint-disable header/header */ +import { promises as fs } from 'fs'; import * as http from 'http'; import * as url from 'url'; +const CHE_CONFIG_KEYBINDINGS_PATH = '/checode-config/keybindings.json'; + +/** + * Reads keybindings.json content from the ConfigMap mount path. + * Returns the raw JSON string if the file exists, undefined otherwise. + */ +export async function getCheInitialKeybindings(): Promise { + try { + return await fs.readFile(CHE_CONFIG_KEYBINDINGS_PATH, 'utf-8'); + } catch { + return undefined; + } +} + export function getCheRedirectLocation(req: http.IncomingMessage, newQuery: any): string { let newLocation; // Grab headers diff --git a/code/src/vs/server/node/webClientServer.ts b/code/src/vs/server/node/webClientServer.ts index a01772faa2ee..6902eca9af1f 100644 --- a/code/src/vs/server/node/webClientServer.ts +++ b/code/src/vs/server/node/webClientServer.ts @@ -5,7 +5,7 @@ import { createReadStream, promises } from 'fs'; import * as http from 'http'; -import { getCheRedirectLocation } from './che/webClientServer.js'; +import { getCheInitialKeybindings, getCheRedirectLocation } from './che/webClientServer.js'; import * as url from 'url'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; @@ -387,7 +387,8 @@ export class WebClientServer { folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), productConfiguration, - callbackRoute: callbackRoute + callbackRoute: callbackRoute, + cheInitialKeybindings: await getCheInitialKeybindings(), }; const cookies = cookie.parse(req.headers.cookie || ''); diff --git a/code/src/vs/workbench/browser/che/web.ts b/code/src/vs/workbench/browser/che/web.ts index 64d3251c8b8f..9f34f143acc7 100644 --- a/code/src/vs/workbench/browser/che/web.ts +++ b/code/src/vs/workbench/browser/che/web.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (c) 2025 Red Hat, Inc. + * Copyright (c) 2025-2026 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -9,10 +9,91 @@ ***********************************************************************/ /* eslint-disable header/header */ +import { VSBuffer } from '../../../base/common/buffer.js'; +import * as json from '../../../base/common/json.js'; +import { IFileService } from '../../../platform/files/common/files.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { IPolicyService, NullPolicyService } from '../../../platform/policy/common/policy.js'; import { PolicyChannelClient } from '../../../platform/policy/common/policyIpc.js'; +import { IUserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js'; import { IRemoteAgentService } from '../../services/remote/common/remoteAgentService.js'; +import { IUserDataInitializer } from '../../services/userData/browser/userDataInit.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; + +const CONFIGMAP_SOURCE_MARKER = 'configmap'; + +interface KeybindingEntry { + key: string; + command: string; + when?: string; + args?: unknown; + _source?: string; +} + +/** + * Merges keybindings from ConfigMap into IndexedDB on every workspace start. + * + * Strategy: admin keybindings (from ConfigMap) are appended at the end of the + * array so they take priority over user keybindings (VS Code processes + * keybindings top-to-bottom, later entries win). Admin entries are tagged with + * `"_source": "configmap"` so they can be identified and replaced on the next + * start when the ConfigMap content changes. + */ +export class CheKeybindingsInitializer implements IUserDataInitializer { + + constructor( + private readonly initialKeybindings: string, + private readonly fileService: IFileService, + private readonly userDataProfilesService: IUserDataProfilesService, + private readonly logService: ILogService, + ) { } + + async requiresInitialization(): Promise { + return true; + } + + async whenInitializationFinished(): Promise { } + + async initializeRequiredResources(): Promise { + const resource = this.userDataProfilesService.defaultProfile.keybindingsResource; + + let configmapEntries: KeybindingEntry[]; + try { + const parsed = json.parse(this.initialKeybindings); + if (!Array.isArray(parsed)) { + this.logService.warn('[Che] ConfigMap keybindings.json is not a JSON array, skipping.'); + return; + } + configmapEntries = parsed; + } catch (e) { + this.logService.warn('[Che] ConfigMap keybindings.json is not valid JSON, skipping.', e); + return; + } + + const markedEntries: KeybindingEntry[] = configmapEntries.map(entry => ({ ...entry, _source: CONFIGMAP_SOURCE_MARKER })); + + let userEntries: KeybindingEntry[] = []; + if (await this.fileService.exists(resource)) { + try { + const content = (await this.fileService.readFile(resource)).value.toString(); + const existing = json.parse(content); + if (Array.isArray(existing)) { + userEntries = existing.filter((e: KeybindingEntry) => e._source !== CONFIGMAP_SOURCE_MARKER); + } + } catch (e) { + this.logService.warn('[Che] Failed to parse existing keybindings. Aborting to prevent data loss.', e); + return; + } + } + + const merged = [...userEntries, ...markedEntries]; + this.logService.info(`[Che] Merging keybindings: ${userEntries.length} user + ${markedEntries.length} admin (ConfigMap).`); + await this.fileService.writeFile(resource, VSBuffer.fromString(JSON.stringify(merged, null, '\t'))); + } + + async initializeInstalledExtensions(_instantiationService: IInstantiationService): Promise { } + async initializeOtherResources(_instantiationService: IInstantiationService): Promise { } +} // Get policy service from remote agent if available, otherwise use NullPolicyService export function getPolicyService(remoteAgentService: IRemoteAgentService, logService: ILogService, remoteAuthority?: string): IPolicyService { diff --git a/code/src/vs/workbench/browser/web.api.ts b/code/src/vs/workbench/browser/web.api.ts index dcb9af34371b..e43f9260a8a0 100644 --- a/code/src/vs/workbench/browser/web.api.ts +++ b/code/src/vs/workbench/browser/web.api.ts @@ -300,6 +300,12 @@ export interface IWorkbenchConstructionOptions { //#endregion + /** + * Initial keybindings content from ConfigMap (JSON array string). + * When set, keybindings are merged into IndexedDB on every workspace start. + */ + readonly cheInitialKeybindings?: string; + //#region Profile options /** diff --git a/code/src/vs/workbench/browser/web.main.ts b/code/src/vs/workbench/browser/web.main.ts index c983682a0c93..17374a290dbd 100644 --- a/code/src/vs/workbench/browser/web.main.ts +++ b/code/src/vs/workbench/browser/web.main.ts @@ -96,7 +96,7 @@ import { ISecretStorageService } from '../../platform/secrets/common/secrets.js' import { TunnelSource } from '../services/remote/common/tunnelModel.js'; import { mainWindow } from '../../base/browser/window.js'; import { INotificationService, Severity } from '../../platform/notification/common/notification.js'; -import { getPolicyService } from './che/web.js'; +import { CheKeybindingsInitializer, getPolicyService } from './che/web.js'; import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js'; import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js'; @@ -439,6 +439,9 @@ export class BrowserMain extends Disposable { if (environmentService.options.profile) { userDataInitializers.push(new UserDataProfileInitializer(environmentService, fileService, userDataProfileService, storageService, logService, uriIdentityService, requestService)); } + if (environmentService.options.cheInitialKeybindings) { + userDataInitializers.push(new CheKeybindingsInitializer(environmentService.options.cheInitialKeybindings, fileService, userDataProfilesService, logService)); + } const userDataInitializationService = new UserDataInitializationService(userDataInitializers); serviceCollection.set(IUserDataInitializationService, userDataInitializationService); diff --git a/rebase.sh b/rebase.sh index 035730a097d9..0371b31933ab 100755 --- a/rebase.sh +++ b/rebase.sh @@ -428,6 +428,8 @@ resolve_conflicts() { apply_changes_multi_line "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/web.main.ts" ]]; then apply_changes_multi_line "$conflictingFile" + elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/web.api.ts" ]]; then + apply_changes_multi_line "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/server/node/serverServices.ts" ]]; then apply_changes_multi_line "$conflictingFile" elif [[ "$conflictingFile" == "code/src/vs/server/node/serverEnvironmentService.ts" ]]; then