Skip to content

Commit 7ae0ca0

Browse files
authored
feat(extensions): Introduce install endpoints for configuration (#823)
* Introduce install endpoints Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Add tests Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Fix sonarqube issues Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Simplify plugin update Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Deduplicate mock Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Deduplicate not found Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Deduplicate Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Fix configYaml body Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Fix missing requireInitializedInstallationDataService Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> --------- Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
1 parent bc300a4 commit 7ae0ca0

14 files changed

Lines changed: 872 additions & 215 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-marketplace-backend': minor
3+
'@red-hat-developer-hub/backstage-plugin-marketplace-common': minor
4+
---
5+
6+
Introduces POST endpoints for updating dynamic plugins and packages installation configuration: `/package/:namespace/:name/configuration` and `/plugin/:namespace/:name/configuration`.
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
2+
rules: {
3+
'jest/expect-expect': [
4+
'error',
5+
{
6+
assertFunctionNames: ['expect*'],
7+
},
8+
],
9+
},
10+
});

workspaces/marketplace/plugins/marketplace-backend/__fixtures__/mockData.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,16 @@ export const mockFileInstallationStorage = {
159159
.map(name => mockPluginsMap.get(name));
160160
return stringify(packages);
161161
}),
162+
updatePackage: jest.fn(),
163+
updatePackages: jest.fn(),
162164
} as unknown as jest.Mocked<FileInstallationStorage>;
163165

164166
export const mockInstallationDataService = {
165167
getPluginConfig: jest.fn(),
166168
getPackageConfig: jest.fn(),
167169
getInitializationError: jest.fn().mockReturnValue(undefined),
170+
updatePackageConfig: jest.fn(),
171+
updatePluginConfig: jest.fn(),
168172
} as unknown as jest.Mocked<InstallationDataService>;
169173

170174
export const mockMarketplaceApi = {

workspaces/marketplace/plugins/marketplace-backend/src/installation/FileInstallationStorage.test.ts

Lines changed: 217 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
* limitations under the License.
1515
*/
1616

17+
import fs from 'fs';
18+
import { resolve } from 'path';
19+
import { parse, stringify } from 'yaml';
1720
import {
1821
mockDynamicPackage11,
1922
mockDynamicPackage12,
2023
mockDynamicPackage21,
2124
mockPackages,
2225
} from '../../__fixtures__/mockData';
2326
import { FileInstallationStorage } from './FileInstallationStorage';
24-
import { resolve } from 'path';
25-
import { stringify } from 'yaml';
2627

2728
describe('FileInstallationStorage', () => {
2829
describe('initialize', () => {
@@ -72,7 +73,7 @@ describe('FileInstallationStorage', () => {
7273
expect(() => {
7374
fileInstallationStorage.initialize();
7475
}).toThrow(
75-
"Failed to load 'extensions.installation.saveToSingleFile.file'. Invalid installation configuration, 'plugins' field must be a list",
76+
"Invalid installation configuration, 'plugins' field must be a list",
7677
);
7778
});
7879

@@ -88,7 +89,7 @@ describe('FileInstallationStorage', () => {
8889
expect(() => {
8990
fileInstallationStorage.initialize();
9091
}).toThrow(
91-
"Invalid installation configuration, 'package' field in each package item must be a non-empty string",
92+
"Invalid installation configuration, 'package' field in package item must be a non-empty string",
9293
);
9394
});
9495
});
@@ -133,4 +134,216 @@ describe('FileInstallationStorage', () => {
133134
).toEqual(stringify([mockDynamicPackage11, mockDynamicPackage12]));
134135
});
135136
});
137+
138+
describe('updatePackage', () => {
139+
afterEach(() => {
140+
fs.writeFileSync(
141+
resolve(__dirname, '../../__fixtures__/data/validPluginsConfig.yaml'),
142+
stringify({
143+
plugins: [
144+
mockDynamicPackage11,
145+
mockDynamicPackage12,
146+
mockDynamicPackage21,
147+
],
148+
}),
149+
);
150+
});
151+
152+
it('should update existing package', () => {
153+
const configFileName = resolve(
154+
__dirname,
155+
'../../__fixtures__/data/validPluginsConfig.yaml',
156+
);
157+
const updatedPackage = {
158+
...mockDynamicPackage21,
159+
disabled: false,
160+
};
161+
const fileInstallationStorage = new FileInstallationStorage(
162+
configFileName,
163+
);
164+
fileInstallationStorage.initialize();
165+
166+
fileInstallationStorage.updatePackage(
167+
'./dynamic-plugins/dist/package21-backend-dynamic',
168+
stringify(updatedPackage),
169+
);
170+
171+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
172+
const configYaml = parse(updatedCatalogInfoYaml);
173+
expect(configYaml.plugins[0]).toEqual(mockDynamicPackage11);
174+
expect(configYaml.plugins[1]).toEqual(mockDynamicPackage12);
175+
expect(configYaml.plugins[2]).toEqual(updatedPackage);
176+
});
177+
178+
it('should add new package', () => {
179+
const configFileName = resolve(
180+
__dirname,
181+
'../../__fixtures__/data/validPluginsConfig.yaml',
182+
);
183+
const packageName = './dynamic-plugins/dist/package3-backend-dynamic';
184+
const newPackage = {
185+
package: packageName,
186+
disabled: true,
187+
};
188+
const fileInstallationStorage = new FileInstallationStorage(
189+
configFileName,
190+
);
191+
fileInstallationStorage.initialize();
192+
193+
fileInstallationStorage.updatePackage(packageName, stringify(newPackage));
194+
195+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
196+
const configYaml = parse(updatedCatalogInfoYaml);
197+
expect(configYaml.plugins[0]).toEqual(mockDynamicPackage11);
198+
expect(configYaml.plugins[1]).toEqual(mockDynamicPackage12);
199+
expect(configYaml.plugins[2]).toEqual(mockDynamicPackage21);
200+
expect(configYaml.plugins[3]).toEqual(newPackage);
201+
});
202+
203+
it('should throw on bad newConfig format', async () => {
204+
const configFileName = resolve(
205+
__dirname,
206+
'../../__fixtures__/data/validPluginsConfig.yaml',
207+
);
208+
const fileInstallationStorage = new FileInstallationStorage(
209+
configFileName,
210+
);
211+
fileInstallationStorage.initialize();
212+
213+
expect(() => {
214+
fileInstallationStorage.updatePackage(
215+
mockDynamicPackage11.package,
216+
'badConfig',
217+
);
218+
}).toThrow(
219+
'Invalid installation configuration, package item must be a map',
220+
);
221+
});
222+
});
223+
224+
describe('updatePackages', () => {
225+
afterEach(() => {
226+
fs.writeFileSync(
227+
resolve(__dirname, '../../__fixtures__/data/validPluginsConfig.yaml'),
228+
stringify({
229+
plugins: [
230+
mockDynamicPackage11,
231+
mockDynamicPackage12,
232+
mockDynamicPackage21,
233+
],
234+
}),
235+
);
236+
});
237+
238+
it('should update existing plugin', () => {
239+
const configFileName = resolve(
240+
__dirname,
241+
'../../__fixtures__/data/validPluginsConfig.yaml',
242+
);
243+
const addedPackage = {
244+
package: './dynamic-plugins/dist/package11-backend-module-dynamic',
245+
disabled: true,
246+
};
247+
const updatedPlugin = [
248+
{
249+
...mockDynamicPackage11,
250+
disabled: false,
251+
pluginConfig: {
252+
plugin1: {
253+
setting: true,
254+
},
255+
},
256+
},
257+
{
258+
...mockDynamicPackage12,
259+
disabled: false,
260+
pluginConfig: {
261+
dynamicPlugins: {
262+
frontend: {
263+
'default.package12': {
264+
mountpoints: [
265+
{
266+
mountPoint: 'entity.page.image-registry/cards',
267+
importName: 'Package12Page',
268+
},
269+
],
270+
},
271+
},
272+
},
273+
},
274+
},
275+
addedPackage,
276+
];
277+
const fileInstallationStorage = new FileInstallationStorage(
278+
configFileName,
279+
);
280+
fileInstallationStorage.initialize();
281+
282+
fileInstallationStorage.updatePackages(
283+
new Set([
284+
mockDynamicPackage11.package,
285+
mockDynamicPackage12.package,
286+
addedPackage.package,
287+
]),
288+
stringify(updatedPlugin),
289+
);
290+
291+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
292+
const configYaml = parse(updatedCatalogInfoYaml);
293+
expect(configYaml.plugins).toEqual([
294+
mockDynamicPackage21,
295+
...updatedPlugin,
296+
]);
297+
});
298+
299+
it('should add new plugin', () => {
300+
const configFileName = resolve(
301+
__dirname,
302+
'../../__fixtures__/data/validPluginsConfig.yaml',
303+
);
304+
const pluginName = './dynamic-plugins/dist/package3-backend-dynamic';
305+
const newPlugin = [
306+
{
307+
package: pluginName,
308+
disabled: true,
309+
},
310+
];
311+
const fileInstallationStorage = new FileInstallationStorage(
312+
configFileName,
313+
);
314+
fileInstallationStorage.initialize();
315+
316+
fileInstallationStorage.updatePackages(
317+
new Set([pluginName]),
318+
stringify(newPlugin),
319+
);
320+
321+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
322+
const configYaml = parse(updatedCatalogInfoYaml);
323+
expect(configYaml.plugins[0]).toEqual(mockDynamicPackage11);
324+
expect(configYaml.plugins[1]).toEqual(mockDynamicPackage12);
325+
expect(configYaml.plugins[2]).toEqual(mockDynamicPackage21);
326+
expect(configYaml.plugins[3]).toEqual(newPlugin[0]);
327+
});
328+
329+
it('should throw on bad newConfig format', async () => {
330+
const configFileName = resolve(
331+
__dirname,
332+
'../../__fixtures__/data/validPluginsConfig.yaml',
333+
);
334+
const fileInstallationStorage = new FileInstallationStorage(
335+
configFileName,
336+
);
337+
fileInstallationStorage.initialize();
338+
339+
expect(() => {
340+
fileInstallationStorage.updatePackages(
341+
new Set(mockDynamicPackage11.package),
342+
'package: badFormat',
343+
);
344+
}).toThrow(
345+
'Invalid installation configuration, plugin packages must be a list',
346+
);
347+
});
348+
});
136349
});

workspaces/marketplace/plugins/marketplace-backend/src/installation/FileInstallationStorage.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@
1616

1717
import fs from 'fs';
1818

19+
import { Document, isMap, parseDocument, type YAMLMap, YAMLSeq } from 'yaml';
1920
import {
20-
Document,
21-
isMap,
22-
parseDocument,
23-
type YAMLMap,
24-
type YAMLSeq,
25-
} from 'yaml';
26-
import { validateConfigurationFormat } from '../validation/configValidation';
21+
validateConfigurationFormat,
22+
validatePackageFormat,
23+
validatePluginFormat,
24+
} from '../validation/configValidation';
2725
import {
2826
InstallationInitError,
2927
InstallationInitErrorReason,
@@ -33,7 +31,9 @@ import type { JsonValue } from '@backstage/types';
3331
export interface InstallationStorage {
3432
initialize?(): void;
3533
getPackage(packageName: string): string | undefined;
34+
updatePackage(packageName: string, newConfig: string): void;
3635
getPackages(packageNames: Set<string>): string | undefined;
36+
updatePackages(packageNames: Set<string>, newConfig: string): void;
3737
}
3838

3939
export class FileInstallationStorage implements InstallationStorage {
@@ -61,6 +61,10 @@ export class FileInstallationStorage implements InstallationStorage {
6161
);
6262
}
6363

64+
private save() {
65+
fs.writeFileSync(this.configFile, this.config.toString({ lineWidth: 0 }));
66+
}
67+
6468
initialize(): void {
6569
if (!fs.existsSync(this.configFile)) {
6670
throw new InstallationInitError(
@@ -93,4 +97,44 @@ export class FileInstallationStorage implements InstallationStorage {
9397
}
9498
return res.length === 0 ? undefined : this.toStringYaml(res);
9599
}
100+
101+
updatePackage(packageName: string, newConfig: string): void {
102+
const newNode = parseDocument(newConfig).contents;
103+
validatePackageFormat(newNode, packageName);
104+
105+
const packages = this.config.get('plugins') as YAMLSeq<
106+
YAMLMap<string, JsonValue>
107+
>;
108+
109+
const existingPackage = packages.items.find(
110+
item => item.get('package') === packageName,
111+
);
112+
if (existingPackage) {
113+
existingPackage.items = newNode.items;
114+
} else {
115+
packages.items.push(newNode);
116+
}
117+
this.save();
118+
}
119+
120+
updatePackages(packageNames: Set<string>, newConfig: string): void {
121+
const newNodes = parseDocument(newConfig);
122+
validatePluginFormat(newNodes, packageNames);
123+
124+
const packages = this.config.get('plugins') as YAMLSeq<
125+
YAMLMap<string, JsonValue>
126+
>;
127+
128+
const updatedPackages = new YAMLSeq<YAMLMap<string, JsonValue>>();
129+
for (const item of packages.items) {
130+
const name = item.get('package') as string;
131+
if (!packageNames.has(name)) {
132+
updatedPackages.items.push(item); // keep unchanged package of different plugin
133+
}
134+
}
135+
updatedPackages.items.push(...newNodes.contents.items);
136+
137+
this.config.set('plugins', updatedPackages);
138+
this.save();
139+
}
96140
}

0 commit comments

Comments
 (0)