Skip to content

Commit 4a9614d

Browse files
feat: allow to configure product.json using configmap (che-incubator#521)
Signed-off-by: vitaliy-guliy <vgulyy@redhat.com>
1 parent dcabb47 commit 4a9614d

5 files changed

Lines changed: 291 additions & 148 deletions

File tree

launcher/package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

launcher/src/editor-configurations.ts

Lines changed: 94 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,44 @@
1010

1111
import * as k8s from '@kubernetes/client-node';
1212
import * as fs from './fs-extra.js';
13+
import { ProductJSON } from './product-json.js';
14+
import { mergeFirstWithSecond, parseJSON } from './json-utils.js';
1315

1416
const CONFIGMAP_NAME = 'vscode-editor-configurations';
1517
const REMOTE_SETTINGS_PATH = '/checode/remote/data/Machine/settings.json';
1618

1719
const enum EditorConfigs {
1820
Settings = 'settings.json',
1921
Extensions = 'extensions.json',
22+
Product = 'product.json',
2023
}
2124

22-
export class EditorConfigurations {
23-
private coreV1API: k8s.CoreV1Api;
25+
/**
26+
* See following documentation for details
27+
* https://eclipse.dev/che/docs/stable/administration-guide/editor-configurations-for-microsoft-visual-studio-code/
28+
*/
2429

30+
export class EditorConfigurations {
2531
constructor(private readonly workspaceFilePath?: string) {}
2632

2733
async configure(): Promise<void> {
28-
console.log('# Checking if editor configurations are provided...');
34+
console.log(`# Checking for editor configurations provided by '${CONFIGMAP_NAME}' Config Map...`);
35+
36+
if (!process.env.DEVWORKSPACE_NAMESPACE) {
37+
console.log(' > process.env.DEVWORKSPACE_NAMESPACE is not set, skip this step');
38+
return;
39+
}
2940

3041
try {
3142
const configmap = await this.getConfigmap();
3243
if (!configmap || !configmap.data) {
33-
console.log(' > Editor configurations are not provided');
44+
console.log(` > Config Map ${CONFIGMAP_NAME} is not provided, skip this step`);
3445
return;
3546
}
3647

37-
const settingsPromise = this.configureSettings(configmap);
38-
const extensionsPromise = this.configureExtensions(configmap);
39-
40-
await Promise.all([settingsPromise, extensionsPromise]);
48+
await this.configureSettings(configmap);
49+
await this.configureExtensions(configmap);
50+
await this.configureProductJSON(configmap);
4151
} catch (error) {
4252
console.log(` > Failed to apply editor configurations ${error}`);
4353
}
@@ -46,134 +56,120 @@ export class EditorConfigurations {
4656
private async configureSettings(configmap: k8s.V1ConfigMap): Promise<void> {
4757
const configmapContent = configmap.data![EditorConfigs.Settings];
4858
if (!configmapContent) {
49-
console.log(` > ${EditorConfigs.Settings} is not provided in the ${CONFIGMAP_NAME} configmap`);
50-
return;
51-
} else {
52-
console.log(
53-
` > ${EditorConfigs.Settings} is provided in the ${CONFIGMAP_NAME} configmap, trying to apply it...`
54-
);
55-
}
56-
57-
const settingsFromConfigmap = parseJsonFrom(configmapContent);
58-
if (!settingsFromConfigmap) {
59-
console.log(
60-
` > Can not apply editor configurations: failed to parse ${EditorConfigs.Settings} data from the ${CONFIGMAP_NAME} configmap`
61-
);
6259
return;
6360
}
6461

65-
let remoteSettingsJson;
66-
if (await fs.fileExists(REMOTE_SETTINGS_PATH)) {
67-
console.log(` > File with settings is found: ${REMOTE_SETTINGS_PATH}`);
62+
console.log(' > Configure editor settings...');
6863

69-
const remoteSettingsContent = await fs.readFile(REMOTE_SETTINGS_PATH);
70-
remoteSettingsJson = parseJsonFrom(remoteSettingsContent);
71-
} else {
72-
console.log(` > File with settings is not found, creating a new one: ${REMOTE_SETTINGS_PATH}`);
73-
remoteSettingsJson = {};
74-
}
64+
try {
65+
const settingsFromConfigmap = parseJSON(configmapContent, {
66+
errorMessage: 'Configmap content is not valid.',
67+
});
68+
69+
let remoteSettingsJson;
70+
if (await fs.fileExists(REMOTE_SETTINGS_PATH)) {
71+
console.log(` > Found setings file: ${REMOTE_SETTINGS_PATH}`);
72+
const remoteSettingsContent = await fs.readFile(REMOTE_SETTINGS_PATH);
73+
remoteSettingsJson = parseJSON(remoteSettingsContent, {
74+
errorMessage: 'Settings.json file is not valid.',
75+
});
76+
} else {
77+
console.log(` > Creating settings file: ${REMOTE_SETTINGS_PATH}`);
78+
remoteSettingsJson = {};
79+
}
7580

76-
const mergedSettings = { ...remoteSettingsJson, ...settingsFromConfigmap };
77-
const json = JSON.stringify(mergedSettings, null, '\t');
78-
await fs.writeFile(REMOTE_SETTINGS_PATH, json);
81+
const mergedSettings = { ...remoteSettingsJson, ...settingsFromConfigmap };
82+
const json = JSON.stringify(mergedSettings, null, '\t');
83+
await fs.writeFile(REMOTE_SETTINGS_PATH, json);
7984

80-
console.log(` > ${EditorConfigs.Settings} configs were applied successfully!`);
85+
console.log(' > Editor settings have been configured.');
86+
} catch (error) {
87+
console.log('Failed to configure editor settings.', error);
88+
}
8189
}
8290

8391
private async configureExtensions(configmap: k8s.V1ConfigMap): Promise<void> {
8492
const configmapContent = configmap.data![EditorConfigs.Extensions];
8593
if (!configmapContent) {
86-
console.log(` > ${EditorConfigs.Extensions} is not provided in the ${CONFIGMAP_NAME} configmap`);
8794
return;
88-
} else {
89-
console.log(
90-
` > ${EditorConfigs.Extensions} is provided in the ${CONFIGMAP_NAME} configmap, trying to apply it...`
91-
);
9295
}
9396

94-
const extensionsFromConfigmap = parseJsonFrom(configmapContent);
95-
if (!extensionsFromConfigmap) {
96-
console.log(
97-
` > Can not apply editor configurations: failed to parse ${EditorConfigs.Extensions} data from the ${CONFIGMAP_NAME} configmap`
98-
);
99-
return;
100-
}
97+
console.log(' > Configure workspace extensions...');
10198

102-
if (this.workspaceFilePath && (await fs.fileExists(this.workspaceFilePath))) {
103-
console.log(` > File with configs is found: ${this.workspaceFilePath}`);
99+
try {
100+
const extensionsFromConfigmap = parseJSON(configmapContent, {
101+
errorMessage: 'Configmap content is not valid.',
102+
});
104103

105-
const workspaceFileContent = await fs.readFile(this.workspaceFilePath);
106-
const workspaceConfigData = parseJsonFrom(workspaceFileContent);
107-
if (!workspaceConfigData) {
108-
console.log(
109-
` > Can not apply configurations for ${EditorConfigs.Extensions}: failed to parse data from the ${this.workspaceFilePath}`
110-
);
104+
if (!this.workspaceFilePath) {
105+
console.log(' > Missing workspace file. Skip this step.');
111106
return;
112107
}
113108

114-
const extensionsSection = 'extensions';
115-
if (!workspaceConfigData[extensionsSection]) {
116-
console.log(' > Extensions section is absent in the workspace file, creating a new one...');
117-
workspaceConfigData[extensionsSection] = {};
109+
if (!(await fs.fileExists(this.workspaceFilePath))) {
110+
console.log(` > Unable to find workspace file: ${this.workspaceFilePath}. Skip this step.`);
111+
return;
118112
}
119113

120-
workspaceConfigData[extensionsSection] = {
121-
...workspaceConfigData[extensionsSection],
114+
console.log(` > Found workspace file: ${this.workspaceFilePath}`);
115+
116+
const workspaceFileContent = await fs.readFile(this.workspaceFilePath);
117+
const workspaceConfigData = parseJSON(workspaceFileContent, {
118+
errorMessage: 'Workspace file is not valid.',
119+
});
120+
121+
workspaceConfigData['extensions'] = {
122+
...(workspaceConfigData['extensions'] || {}),
122123
...extensionsFromConfigmap,
123124
};
124125

125126
const json = JSON.stringify(workspaceConfigData, null, '\t');
126127
await fs.writeFile(this.workspaceFilePath, json);
127-
console.log(` > ${EditorConfigs.Extensions} was applied successfully!`);
128-
} else {
129-
console.log(` > Workspace Config File with is not found: ${this.workspaceFilePath}`);
130-
// it's not possible to store extensions for the Remote scope,
131-
// for sinle root mode extensions should come with a project, like: /project/.vscode/extensions.json - Folder scope
132-
// so, we don't store extensions configs in another place if there is no workspace config file
133-
//
128+
console.log(' > Workspace extensions have been configured.');
129+
} catch (error) {
130+
console.log('Failed to configure workspace extensions.', error);
131+
}
132+
}
133+
134+
private async configureProductJSON(configmap: k8s.V1ConfigMap): Promise<void> {
135+
const configmapContent = configmap.data![EditorConfigs.Product];
136+
if (!configmapContent) {
134137
return;
135138
}
139+
140+
console.log(' > Configure product.json ...');
141+
142+
try {
143+
const productFromConfigmap = parseJSON(configmapContent, {
144+
errorMessage: 'Configmap content is not valid.',
145+
});
146+
147+
const product = new ProductJSON();
148+
await product.load();
149+
150+
mergeFirstWithSecond(productFromConfigmap, product.get());
151+
152+
await product.save();
153+
154+
console.log(' > product.json have been configured.');
155+
} catch (error) {
156+
console.log(`Failed to configure ${EditorConfigs.Product}.`, error);
157+
}
136158
}
137159

138160
private async getConfigmap(): Promise<k8s.V1ConfigMap | undefined> {
139-
const coreV1API = this.getCoreApi();
140-
const namespace = process.env.DEVWORKSPACE_NAMESPACE;
141-
if (!namespace) {
142-
console.log(
143-
' > Error: Can not get Configmap with editor configurations - DEVWORKSPACE_NAMESPACE env variable is not defined'
144-
);
145-
return undefined;
146-
}
161+
const k8sConfig = new k8s.KubeConfig();
162+
k8sConfig.loadFromCluster();
163+
const coreV1API = k8sConfig.makeApiClient(k8s.CoreV1Api);
147164

148165
try {
149-
const { body } = await coreV1API.readNamespacedConfigMap(CONFIGMAP_NAME, namespace);
166+
const { body } = await coreV1API.readNamespacedConfigMap(CONFIGMAP_NAME, process.env.DEVWORKSPACE_NAMESPACE!);
150167
return body;
151168
} catch (error) {
152169
console.log(
153-
`# > Warning: Can not get Configmap with editor configurations: ${error.message}, status code: ${error?.response?.statusCode}`
170+
` > Warning: Can not get Configmap with editor configurations: ${error.message}, status code: ${error?.response?.statusCode}`
154171
);
155172
return undefined;
156173
}
157174
}
158-
159-
getCoreApi(): k8s.CoreV1Api {
160-
if (!this.coreV1API) {
161-
const k8sConfig = new k8s.KubeConfig();
162-
k8sConfig.loadFromCluster();
163-
this.coreV1API = k8sConfig.makeApiClient(k8s.CoreV1Api);
164-
}
165-
return this.coreV1API;
166-
}
167-
}
168-
169-
export function parseJsonFrom(content: string): any {
170-
if (!content) {
171-
return undefined;
172-
}
173-
174-
try {
175-
return JSON.parse(content);
176-
} catch (parseError) {
177-
console.error(` > Error parsing JSON: ${parseError}`);
178-
}
179175
}

launcher/src/json-utils.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**********************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
***********************************************************************/
10+
11+
/**
12+
* Recursively copies all properties from the first object to the next.
13+
* If the property exists in the next object, its value will be updated.
14+
*/
15+
export function mergeFirstWithSecond(first: any, second: any) {
16+
if (Array.isArray(first)) {
17+
if (!Array.isArray(second)) {
18+
// incompatiblity of types :: skip and do nothing
19+
return;
20+
}
21+
22+
for (const element of first) {
23+
if (second.includes(element)) {
24+
continue;
25+
}
26+
27+
second.push(element);
28+
}
29+
30+
return;
31+
}
32+
33+
for (const key of Object.keys(first)) {
34+
if (second[key] === undefined) {
35+
// if second object does not contain the property, copy it
36+
second[key] = first[key];
37+
} else {
38+
// if second object contains the same property, check for options
39+
if ('string' === typeof first[key] && 'string' === typeof second[key]) {
40+
// if the property is string, update the value
41+
second[key] = first[key];
42+
} else if ('number' === typeof first[key] && 'number' === typeof second[key]) {
43+
// if the property is number, update the value
44+
second[key] = first[key];
45+
} else if ('object' === typeof first[key] && 'object' === typeof second[key]) {
46+
// if the property is object, merge values
47+
mergeFirstWithSecond(first[key], second[key]);
48+
}
49+
50+
// in case of incompatibility of types, do nothing
51+
}
52+
}
53+
}
54+
55+
export function parseJSON(content: string, options?: { errorMessage: string }) {
56+
try {
57+
return JSON.parse(content);
58+
} catch (error) {
59+
if (error.message && options && options.errorMessage) {
60+
error.message = `${options.errorMessage} ${error.message}`;
61+
}
62+
63+
throw error;
64+
}
65+
}

0 commit comments

Comments
 (0)