Skip to content

Commit 3973839

Browse files
authored
feat(extensions): Introduce disable endpoint for configuration (#921)
* Introduce disable endpoint Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Separate create and update for disable endpoint Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> * Update changeset Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com> --------- Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
1 parent 3fe9a68 commit 3973839

11 files changed

Lines changed: 502 additions & 108 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 PUT endpoint for enabling or disabling dynamic plugins: `/plugin/:namespace/:name/configuration/disable` and POST endpoint for enabling or disabling dynamic packages `/package/:namespace/:name/configuration/disable` not already existing in `EXTENSIONS_PLUGIN_CONFIG`.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ export const mockFileInstallationStorage = {
161161
}),
162162
updatePackage: jest.fn(),
163163
updatePackages: jest.fn(),
164+
addPackageDisabled: jest.fn(),
165+
setPackagesDisabled: jest.fn(),
164166
} as unknown as jest.Mocked<FileInstallationStorage>;
165167

166168
export const mockInstallationDataService = {
@@ -169,6 +171,8 @@ export const mockInstallationDataService = {
169171
getInitializationError: jest.fn().mockReturnValue(undefined),
170172
updatePackageConfig: jest.fn(),
171173
updatePluginConfig: jest.fn(),
174+
addPackageDisabled: jest.fn(),
175+
setPluginDisabled: jest.fn(),
172176
} as unknown as jest.Mocked<InstallationDataService>;
173177

174178
export const mockMarketplaceApi = {

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

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import {
2424
mockPackages,
2525
} from '../../__fixtures__/mockData';
2626
import { FileInstallationStorage } from './FileInstallationStorage';
27+
import { ConflictError } from '@backstage/errors';
2728

2829
describe('FileInstallationStorage', () => {
30+
const newPackageName = './dynamic-plugins/dist/package3-backend-dynamic';
31+
2932
describe('initialize', () => {
3033
it('should initialize valid config', () => {
3134
const configFileName = resolve(
@@ -170,27 +173,31 @@ describe('FileInstallationStorage', () => {
170173

171174
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
172175
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+
expect(configYaml.plugins).toEqual([
177+
mockDynamicPackage11,
178+
mockDynamicPackage12,
179+
updatedPackage,
180+
]);
176181
});
177182

178183
it('should add new package', () => {
179184
const configFileName = resolve(
180185
__dirname,
181186
'../../__fixtures__/data/validPluginsConfig.yaml',
182187
);
183-
const packageName = './dynamic-plugins/dist/package3-backend-dynamic';
184188
const newPackage = {
185-
package: packageName,
189+
package: newPackageName,
186190
disabled: true,
187191
};
188192
const fileInstallationStorage = new FileInstallationStorage(
189193
configFileName,
190194
);
191195
fileInstallationStorage.initialize();
192196

193-
fileInstallationStorage.updatePackage(packageName, stringify(newPackage));
197+
fileInstallationStorage.updatePackage(
198+
newPackageName,
199+
stringify(newPackage),
200+
);
194201

195202
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
196203
const configYaml = parse(updatedCatalogInfoYaml);
@@ -301,10 +308,9 @@ describe('FileInstallationStorage', () => {
301308
__dirname,
302309
'../../__fixtures__/data/validPluginsConfig.yaml',
303310
);
304-
const pluginName = './dynamic-plugins/dist/package3-backend-dynamic';
305311
const newPlugin = [
306312
{
307-
package: pluginName,
313+
package: newPackageName,
308314
disabled: true,
309315
},
310316
];
@@ -314,7 +320,7 @@ describe('FileInstallationStorage', () => {
314320
fileInstallationStorage.initialize();
315321

316322
fileInstallationStorage.updatePackages(
317-
new Set([pluginName]),
323+
new Set([newPackageName]),
318324
stringify(newPlugin),
319325
);
320326

@@ -346,4 +352,115 @@ describe('FileInstallationStorage', () => {
346352
);
347353
});
348354
});
355+
356+
describe('addPackageDisabled', () => {
357+
afterEach(() => {
358+
fs.writeFileSync(
359+
resolve(__dirname, '../../__fixtures__/data/validPluginsConfig.yaml'),
360+
stringify({
361+
plugins: [
362+
mockDynamicPackage11,
363+
mockDynamicPackage12,
364+
mockDynamicPackage21,
365+
],
366+
}),
367+
);
368+
});
369+
370+
it('should add package with disabled', () => {
371+
const configFileName = resolve(
372+
__dirname,
373+
'../../__fixtures__/data/validPluginsConfig.yaml',
374+
);
375+
const fileInstallationStorage = new FileInstallationStorage(
376+
configFileName,
377+
);
378+
fileInstallationStorage.initialize();
379+
380+
fileInstallationStorage.addPackageDisabled(newPackageName, false);
381+
382+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
383+
const configYaml = parse(updatedCatalogInfoYaml);
384+
expect(configYaml.plugins).toEqual([
385+
mockDynamicPackage11,
386+
mockDynamicPackage12,
387+
mockDynamicPackage21,
388+
{ package: newPackageName, disabled: false },
389+
]);
390+
});
391+
392+
it('should raise ConflictError for package already in the config', () => {
393+
const configFileName = resolve(
394+
__dirname,
395+
'../../__fixtures__/data/validPluginsConfig.yaml',
396+
);
397+
const fileInstallationStorage = new FileInstallationStorage(
398+
configFileName,
399+
);
400+
fileInstallationStorage.initialize();
401+
402+
expect(() => {
403+
fileInstallationStorage.addPackageDisabled(
404+
mockDynamicPackage11.package,
405+
false,
406+
);
407+
}).toThrow(
408+
new ConflictError(
409+
`Package '${mockDynamicPackage11.package}' already exists in the configuration`,
410+
),
411+
);
412+
});
413+
});
414+
415+
describe('setPackagesDisabled', () => {
416+
afterEach(() => {
417+
fs.writeFileSync(
418+
resolve(__dirname, '../../__fixtures__/data/validPluginsConfig.yaml'),
419+
stringify({
420+
plugins: [
421+
mockDynamicPackage11,
422+
mockDynamicPackage12,
423+
mockDynamicPackage21,
424+
],
425+
}),
426+
);
427+
});
428+
429+
it('should set disabled for plugin packages that are already in the config', () => {
430+
const configFileName = resolve(
431+
__dirname,
432+
'../../__fixtures__/data/validPluginsConfig.yaml',
433+
);
434+
const updatedPlugin = [
435+
{
436+
...mockDynamicPackage11,
437+
disabled: false,
438+
},
439+
{
440+
...mockDynamicPackage12,
441+
disabled: false,
442+
},
443+
];
444+
const fileInstallationStorage = new FileInstallationStorage(
445+
configFileName,
446+
);
447+
fileInstallationStorage.initialize();
448+
449+
fileInstallationStorage.setPackagesDisabled(
450+
new Set([
451+
mockDynamicPackage11.package,
452+
mockDynamicPackage12.package,
453+
'./dynamic-plugins/dist/package13-backend-module-dynamic', // not added
454+
]),
455+
false,
456+
);
457+
458+
const updatedCatalogInfoYaml = fs.readFileSync(configFileName, 'utf8');
459+
const configYaml = parse(updatedCatalogInfoYaml);
460+
expect(configYaml.plugins).toEqual([
461+
...updatedPlugin,
462+
mockDynamicPackage21,
463+
]);
464+
});
465+
});
349466
});

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

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

1717
import fs from 'fs';
1818

19-
import { Document, isMap, parseDocument, type YAMLMap, YAMLSeq } from 'yaml';
19+
import { Document, isMap, parseDocument, YAMLMap, YAMLSeq } from 'yaml';
2020
import {
2121
validateConfigurationFormat,
2222
validatePackageFormat,
@@ -27,13 +27,16 @@ import {
2727
InstallationInitErrorReason,
2828
} from '../errors/InstallationInitError';
2929
import type { JsonValue } from '@backstage/types';
30+
import { ConflictError } from '@backstage/errors';
3031

3132
export interface InstallationStorage {
3233
initialize?(): void;
3334
getPackage(packageName: string): string | undefined;
3435
updatePackage(packageName: string, newConfig: string): void;
3536
getPackages(packageNames: Set<string>): string | undefined;
3637
updatePackages(packageNames: Set<string>, newConfig: string): void;
38+
addPackageDisabled(packageName: string, disabled: boolean): void;
39+
setPackagesDisabled(packageNames: Set<string>, disabled: boolean): void;
3740
}
3841

3942
export class FileInstallationStorage implements InstallationStorage {
@@ -45,6 +48,10 @@ export class FileInstallationStorage implements InstallationStorage {
4548
this.config = new Document();
4649
}
4750

51+
private get packages(): YAMLSeq<YAMLMap<string, JsonValue>> {
52+
return this.config.get('plugins') as YAMLSeq<YAMLMap<string, JsonValue>>;
53+
}
54+
4855
private toStringYaml(mapNodes: YAMLMap<string, JsonValue>[]): string {
4956
const tempDoc = new Document(mapNodes);
5057
return tempDoc.toString({ lineWidth: 0 });
@@ -53,10 +60,7 @@ export class FileInstallationStorage implements InstallationStorage {
5360
private getPackageYamlMap(
5461
packageName: string,
5562
): YAMLMap<string, JsonValue> | undefined {
56-
const packages = this.config.get('plugins') as YAMLSeq<
57-
YAMLMap<string, JsonValue>
58-
>;
59-
return packages.items.find(
63+
return this.packages.items.find(
6064
p => isMap(p) && p.get('package') === packageName,
6165
);
6266
}
@@ -102,17 +106,13 @@ export class FileInstallationStorage implements InstallationStorage {
102106
const newNode = parseDocument(newConfig).contents;
103107
validatePackageFormat(newNode, packageName);
104108

105-
const packages = this.config.get('plugins') as YAMLSeq<
106-
YAMLMap<string, JsonValue>
107-
>;
108-
109-
const existingPackage = packages.items.find(
109+
const existingPackage = this.packages.items.find(
110110
item => item.get('package') === packageName,
111111
);
112112
if (existingPackage) {
113113
existingPackage.items = newNode.items;
114114
} else {
115-
packages.items.push(newNode);
115+
this.packages.items.push(newNode);
116116
}
117117
this.save();
118118
}
@@ -121,12 +121,8 @@ export class FileInstallationStorage implements InstallationStorage {
121121
const newNodes = parseDocument(newConfig);
122122
validatePluginFormat(newNodes, packageNames);
123123

124-
const packages = this.config.get('plugins') as YAMLSeq<
125-
YAMLMap<string, JsonValue>
126-
>;
127-
128124
const updatedPackages = new YAMLSeq<YAMLMap<string, JsonValue>>();
129-
for (const item of packages.items) {
125+
for (const item of this.packages.items) {
130126
const name = item.get('package') as string;
131127
if (!packageNames.has(name)) {
132128
updatedPackages.items.push(item); // keep unchanged package of different plugin
@@ -137,4 +133,30 @@ export class FileInstallationStorage implements InstallationStorage {
137133
this.config.set('plugins', updatedPackages);
138134
this.save();
139135
}
136+
137+
addPackageDisabled(packageName: string, disabled: boolean) {
138+
const existingPackage = this.getPackageYamlMap(packageName);
139+
if (existingPackage) {
140+
throw new ConflictError(
141+
`Package '${packageName}' already exists in the configuration`,
142+
);
143+
}
144+
const newPackage = new YAMLMap<string, JsonValue>();
145+
newPackage.set('package', packageName);
146+
newPackage.set('disabled', disabled);
147+
this.packages.add(newPackage);
148+
this.save();
149+
}
150+
151+
setPackagesDisabled(packageNames: Set<string>, disabled: boolean) {
152+
// Sets packages disabled if they are already in the config
153+
for (const item of this.packages.items) {
154+
const name = item.get('package') as string;
155+
if (packageNames.has(name)) {
156+
item.set('disabled', disabled);
157+
}
158+
}
159+
160+
this.save();
161+
}
140162
}

0 commit comments

Comments
 (0)