Skip to content

Commit 2fc6542

Browse files
authored
feat(x2a): add scaffolder-backend-module-x2a and create project template (#2240)
* feat(x2a): add scaffolder-backend-module-x2a and create project template Signed-off-by: Marek Libra <marek.libra@gmail.com> * Set RBAC rules * tests * changeset * yarn.lock * pass token explicitely --------- Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent f088a45 commit 2fc6542

21 files changed

Lines changed: 694 additions & 27 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a': patch
3+
'@red-hat-developer-hub/backstage-plugin-x2a-common': patch
4+
'@red-hat-developer-hub/backstage-plugin-x2a': patch
5+
---
6+
7+
Adding scaffolder software template for creating conversion projects and corresponding action.

workspaces/x2a/app-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ catalog:
9999
target: ../../examples/org.yaml
100100
rules:
101101
- allow: [User, Group]
102+
# x2a Create Project template
103+
- type: file
104+
target: ../../templates/conversion-project-template.yaml
105+
rules:
106+
- allow: [Template]
102107

103108
kubernetes:
104109
# see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
# All the content in this file is just an example
2+
# Set x2a.admin and x2a.user permissions per individual needs.
3+
14
p, role:default/x2aAdmin, x2a.admin, read, allow
25
p, role:default/x2aAdmin, x2a.admin, update, allow
36

4-
p, role:default/x2aUser, x2a.user, update, allow
7+
p, role:default/x2aUser, x2a.user, use, allow
8+
59
g, user:development/guest, role:default/x2aUser
6-
710
g, user:default/mareklibra, role:default/x2aAdmin
11+
g, user:default/elai-shalev, role:default/x2aAdmin
812

9-
###
10-
# For Catalog and Scaffolder templates:
13+
########################################################
14+
# Catalog and Scaffolder templates permissions
15+
# Following settings are for development only.
16+
17+
p, role:default/x2aAdmin, catalog.entity.read, read, allow
18+
p, role:default/x2aAdmin, catalog.entity.create, create, allow
19+
p, role:default/x2aAdmin, catalog.location.read, read, allow
20+
p, role:default/x2aAdmin, catalog.location.create, create, allow
21+
22+
p, role:default/x2aAdmin, scaffolder.action.execute, use, allow
23+
p, role:default/x2aAdmin, scaffolder.template.parameter.read, read, allow
24+
p, role:default/x2aAdmin, scaffolder.template.step.read, read, allow
1125
p, role:default/x2aAdmin, scaffolder.task.create, create, allow
26+
p, role:default/x2aAdmin, scaffolder.task.cancel, cancel, allow
1227
p, role:default/x2aAdmin, scaffolder.task.read, read, allow
13-
p, role:default/x2aAdmin, catalog.entity.create, create, allow
14-
p, role:default/x2aAdmin, catalog.entity.read, read, allow
1528

16-
p, role:default/x2aUser, scaffolder.task.create, create, allow
17-
p, role:default/x2aUser, scaffolder.task.read, read, allow
18-
p, role:default/x2aUser, catalog.entity.create, create, allow
1929
p, role:default/x2aUser, catalog.entity.read, read, allow
30+
p, role:default/x2aUser, catalog.entity.create, create, allow
31+
p, role:default/x2aUser, catalog.location.read, read, allow
32+
p, role:default/x2aUser, catalog.location.create, create, allow
33+
34+
p, role:default/x2aUser, scaffolder.action.execute, use, allow
35+
p, role:default/x2aUser, scaffolder.template.parameter.read, read, allow
36+
p, role:default/x2aUser, scaffolder.template.step.read, read, allow
37+
p, role:default/x2aUser, scaffolder.task.create, create, allow
38+
p, role:default/x2aUser, scaffolder.task.cancel, cancel, allow
39+
p, role:default/x2aUser, scaffolder.task.read, read, allow

workspaces/x2a/packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@backstage/plugin-search-backend-node": "^1.3.17",
4949
"@backstage/plugin-signals-backend": "^0.3.10",
5050
"@backstage/plugin-techdocs-backend": "^2.1.2",
51+
"@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a": "workspace:^",
5152
"@red-hat-developer-hub/backstage-plugin-x2a-backend": "workspace:*",
5253
"app": "link:../app",
5354
"better-sqlite3": "^12.0.0",

workspaces/x2a/packages/backend/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,7 @@ backend.add(import('@backstage/plugin-signals-backend'));
8080

8181
backend.add(import('@red-hat-developer-hub/backstage-plugin-x2a-backend'));
8282

83+
backend.add(
84+
import('@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a'),
85+
);
8386
backend.start();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a
2+
3+
The x2a module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
4+
5+
_This plugin was created through the Backstage CLI_
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a",
3+
"version": "0.1.0",
4+
"license": "Apache-2.0",
5+
"description": "The x2a module for @backstage/plugin-scaffolder-backend",
6+
"main": "src/index.ts",
7+
"types": "src/index.ts",
8+
"publishConfig": {
9+
"access": "public",
10+
"main": "dist/index.cjs.js",
11+
"types": "dist/index.d.ts"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/redhat-developer/rhdh-plugins",
16+
"directory": "workspaces/x2a/plugins/scaffolder-backend-module-x2a"
17+
},
18+
"backstage": {
19+
"role": "backend-plugin-module",
20+
"pluginId": "scaffolder",
21+
"pluginPackage": "@backstage/plugin-scaffolder-backend"
22+
},
23+
"scripts": {
24+
"start": "backstage-cli package start",
25+
"build": "backstage-cli package build",
26+
"lint": "backstage-cli package lint",
27+
"test": "backstage-cli package test",
28+
"clean": "backstage-cli package clean",
29+
"prepack": "backstage-cli package prepack",
30+
"postpack": "backstage-cli package postpack"
31+
},
32+
"dependencies": {
33+
"@backstage/backend-plugin-api": "1.5.0",
34+
"@backstage/plugin-scaffolder-node": "^0.12.1",
35+
"@red-hat-developer-hub/backstage-plugin-x2a-common": "workspace:*"
36+
},
37+
"devDependencies": {
38+
"@backstage/cli": "^0.34.5",
39+
"@backstage/plugin-scaffolder-node-test-utils": "^0.3.5"
40+
},
41+
"files": [
42+
"dist"
43+
]
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## API Report File for "@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { BackendFeature } from '@backstage/backend-plugin-api';
8+
9+
// @public
10+
const scaffolderModule: BackendFeature;
11+
export default scaffolderModule;
12+
13+
```
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
17+
import { createProjectAction } from './createProject';
18+
19+
describe('x2a:project:create', () => {
20+
const mockDiscoveryApi = {
21+
getBaseUrl: jest.fn().mockResolvedValue('http://backstage.example.com'),
22+
getExternalBaseUrl: jest
23+
.fn()
24+
.mockResolvedValue('http://backstage.example.com'),
25+
};
26+
27+
const mockFetch = jest.fn();
28+
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
});
32+
33+
it('should create a project and output projectId and nextUrl', async () => {
34+
const createdProject = {
35+
id: 'project-uuid-123',
36+
abbreviation: 'PRJ',
37+
name: 'My Project',
38+
description: 'A test project',
39+
createdAt: '2025-01-01T00:00:00.000Z',
40+
createdBy: 'user:default/jane',
41+
};
42+
43+
mockFetch.mockResolvedValueOnce({
44+
ok: true,
45+
json: () => Promise.resolve(createdProject),
46+
});
47+
48+
const action = createProjectAction(mockDiscoveryApi);
49+
50+
// The action uses global fetch inside the custom fetchApi; we need to inject our mock.
51+
// createProjectAction builds DefaultApiClient with fetchApi: { fetch: (url, opts) => fetch(url, opts) }.
52+
// So the handler uses the real fetch. We mock global fetch so the client's request is intercepted.
53+
const originalFetch = globalThis.fetch;
54+
globalThis.fetch = mockFetch;
55+
56+
const mockContext = createMockActionContext({
57+
input: {
58+
name: 'My Project',
59+
description: 'A test project',
60+
abbreviation: 'PRJ',
61+
sourceRepoUrl: 'https://github.com/org/repo',
62+
areTargeAndSourceRepoShared: false,
63+
targetRepoBranch: 'main',
64+
},
65+
});
66+
67+
await action.handler(mockContext);
68+
69+
globalThis.fetch = originalFetch;
70+
71+
expect(mockContext.output).toHaveBeenCalledWith(
72+
'projectId',
73+
'project-uuid-123',
74+
);
75+
expect(mockContext.output).toHaveBeenCalledWith(
76+
'nextUrl',
77+
'/x2a/projects/project-uuid-123',
78+
);
79+
});
80+
81+
it('should send name, description (or empty string), and abbreviation in the request body', async () => {
82+
const createdProject = {
83+
id: 'project-uuid-456',
84+
abbreviation: 'ABBR',
85+
name: 'Another Project',
86+
description: '',
87+
createdAt: '2025-01-01T00:00:00.000Z',
88+
createdBy: 'user:default/john',
89+
};
90+
91+
mockFetch.mockImplementation((_url: string, options?: RequestInit) => {
92+
const body = options?.body ? JSON.parse(options.body as string) : {};
93+
expect(body).toEqual({
94+
name: 'Another Project',
95+
description: '',
96+
abbreviation: 'ABBR',
97+
});
98+
return Promise.resolve({
99+
ok: true,
100+
json: () => Promise.resolve(createdProject),
101+
});
102+
});
103+
104+
const originalFetch = globalThis.fetch;
105+
globalThis.fetch = mockFetch;
106+
107+
const action = createProjectAction(mockDiscoveryApi);
108+
const mockContext = createMockActionContext({
109+
input: {
110+
name: 'Another Project',
111+
abbreviation: 'ABBR',
112+
sourceRepoUrl: 'https://github.com/org/repo2',
113+
areTargeAndSourceRepoShared: true,
114+
targetRepoBranch: 'main',
115+
},
116+
});
117+
118+
await action.handler(mockContext);
119+
120+
globalThis.fetch = originalFetch;
121+
122+
expect(mockContext.output).toHaveBeenCalledWith(
123+
'projectId',
124+
'project-uuid-456',
125+
);
126+
expect(mockContext.output).toHaveBeenCalledWith(
127+
'nextUrl',
128+
'/x2a/projects/project-uuid-456',
129+
);
130+
});
131+
132+
it('should include Authorization header when backstageToken is provided', async () => {
133+
const createdProject = {
134+
id: 'project-uuid-789',
135+
abbreviation: 'TKN',
136+
name: 'Token Project',
137+
description: 'With token',
138+
createdAt: '2025-01-01T00:00:00.000Z',
139+
createdBy: 'user:default/alice',
140+
};
141+
142+
mockFetch.mockImplementation((_url: string, options?: RequestInit) => {
143+
const headers = (options?.headers || {}) as Record<string, string>;
144+
expect(headers.Authorization).toBe('Bearer my-backstage-token');
145+
return Promise.resolve({
146+
ok: true,
147+
json: () => Promise.resolve(createdProject),
148+
});
149+
});
150+
151+
const originalFetch = globalThis.fetch;
152+
globalThis.fetch = mockFetch;
153+
154+
const action = createProjectAction(mockDiscoveryApi);
155+
const mockContext = createMockActionContext({
156+
input: {
157+
name: 'Token Project',
158+
description: 'With token',
159+
abbreviation: 'TKN',
160+
sourceRepoUrl: 'https://github.com/org/repo',
161+
areTargeAndSourceRepoShared: false,
162+
targetRepoBranch: 'main',
163+
},
164+
secrets: {
165+
backstageToken: 'my-backstage-token',
166+
},
167+
});
168+
169+
await action.handler(mockContext);
170+
171+
globalThis.fetch = originalFetch;
172+
173+
expect(mockContext.output).toHaveBeenCalledWith(
174+
'projectId',
175+
'project-uuid-789',
176+
);
177+
});
178+
179+
it('should log that the action is running', async () => {
180+
const loggerInfo = jest.fn();
181+
mockFetch.mockResolvedValueOnce({
182+
ok: true,
183+
json: () =>
184+
Promise.resolve({
185+
id: 'log-test-id',
186+
abbreviation: 'LOG',
187+
name: 'Log Test',
188+
description: '',
189+
createdAt: '2025-01-01T00:00:00.000Z',
190+
createdBy: 'user:default/bob',
191+
}),
192+
});
193+
194+
const originalFetch = globalThis.fetch;
195+
globalThis.fetch = mockFetch;
196+
197+
const action = createProjectAction(mockDiscoveryApi);
198+
const mockContext = createMockActionContext({
199+
input: {
200+
name: 'Log Test',
201+
abbreviation: 'LOG',
202+
sourceRepoUrl: 'https://github.com/org/repo',
203+
areTargeAndSourceRepoShared: false,
204+
targetRepoBranch: 'main',
205+
},
206+
logger: {
207+
...createMockActionContext().logger,
208+
info: loggerInfo,
209+
},
210+
});
211+
212+
await action.handler(mockContext);
213+
214+
globalThis.fetch = originalFetch;
215+
216+
expect(loggerInfo).toHaveBeenCalledWith(
217+
'Running x2a:project:create template action for undefined',
218+
);
219+
});
220+
});

0 commit comments

Comments
 (0)