@platform-mesh/portal-server-lib 0.5.43 → 0.5.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/portal-options/auth-config-provider.d.ts +5 -3
- package/dist/portal-options/auth-config-provider.js +28 -10
- package/dist/portal-options/auth-config-provider.js.map +1 -1
- package/dist/portal-options/models/k8s.d.ts +25 -0
- package/dist/portal-options/models/k8s.js +2 -0
- package/dist/portal-options/models/k8s.js.map +1 -0
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.d.ts +1 -1
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.js +15 -32
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.js.map +1 -1
- package/dist/portal-options/services/kcp-k8s.service.d.ts +18 -5
- package/dist/portal-options/services/kcp-k8s.service.js +106 -11
- package/dist/portal-options/services/kcp-k8s.service.js.map +1 -1
- package/package.json +2 -2
- package/src/portal-options/auth-config-provider.spec.ts +480 -58
- package/src/portal-options/auth-config-provider.ts +40 -11
- package/src/portal-options/models/k8s.ts +27 -0
- package/src/portal-options/pm-portal-context.service.spec.ts +4 -0
- package/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts +500 -203
- package/src/portal-options/service-providers/kubernetes-service-providers.service.ts +30 -40
- package/src/portal-options/services/kcp-k8s.service.spec.ts +502 -89
- package/src/portal-options/services/kcp-k8s.service.ts +147 -13
|
@@ -1,115 +1,170 @@
|
|
|
1
|
+
import { K8sRequestContext, K8sResourceDescriptor } from '../models/k8s.js';
|
|
1
2
|
import { KcpKubernetesService } from './kcp-k8s.service.js';
|
|
2
3
|
import type { Request } from 'express';
|
|
3
4
|
|
|
4
|
-
jest.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
const mockListClusterCustomObject = jest.fn();
|
|
6
|
+
const mockReadNamespacedSecret = jest.fn();
|
|
7
|
+
const mockMakeApiClient = jest.fn();
|
|
8
|
+
const mockGetCurrentCluster = jest.fn();
|
|
9
|
+
const mockLoadFromFile = jest.fn();
|
|
10
|
+
const mockAddUser = jest.fn();
|
|
11
|
+
const mockAddContext = jest.fn();
|
|
12
|
+
const mockSetCurrentContext = jest.fn();
|
|
13
|
+
|
|
14
|
+
jest.mock('@kubernetes/client-node', () => ({
|
|
15
|
+
CustomObjectsApi: jest.fn().mockImplementation(() => ({
|
|
16
|
+
listClusterCustomObject: mockListClusterCustomObject,
|
|
17
|
+
})),
|
|
18
|
+
CoreV1Api: jest.fn().mockImplementation(() => ({
|
|
19
|
+
readNamespacedSecret: mockReadNamespacedSecret,
|
|
20
|
+
})),
|
|
21
|
+
KubeConfig: jest.fn().mockImplementation(() => ({
|
|
22
|
+
loadFromFile: mockLoadFromFile,
|
|
23
|
+
addUser: mockAddUser,
|
|
24
|
+
addContext: mockAddContext,
|
|
25
|
+
setCurrentContext: mockSetCurrentContext,
|
|
26
|
+
getCurrentCluster: mockGetCurrentCluster,
|
|
27
|
+
makeApiClient: mockMakeApiClient,
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
22
30
|
|
|
23
31
|
jest.mock('../utils/domain.js', () => ({
|
|
24
32
|
getOrganization: jest.fn(() => 'org-1'),
|
|
25
33
|
}));
|
|
26
34
|
|
|
35
|
+
jest.mock('@kubernetes/client-node/dist/gen/middleware.js', () => ({
|
|
36
|
+
PromiseMiddlewareWrapper: class {
|
|
37
|
+
constructor(public options: any) {}
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
27
41
|
describe('KcpKubernetesService', () => {
|
|
28
42
|
const OLD_ENV = process.env;
|
|
29
43
|
|
|
30
44
|
beforeEach(() => {
|
|
31
|
-
jest.
|
|
32
|
-
process.env = {
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
process.env = {
|
|
47
|
+
...OLD_ENV,
|
|
48
|
+
KUBECONFIG_KCP: '/tmp/kcp.kubeconfig',
|
|
49
|
+
BASE_DOMAINS_DEFAULT: 'example.com',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
mockGetCurrentCluster.mockReturnValue({
|
|
53
|
+
server: 'https://kcp.example.com/base',
|
|
54
|
+
name: 'test-cluster',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
mockMakeApiClient.mockReturnValue({
|
|
58
|
+
listClusterCustomObject: mockListClusterCustomObject,
|
|
59
|
+
readNamespacedSecret: mockReadNamespacedSecret,
|
|
60
|
+
});
|
|
33
61
|
});
|
|
34
62
|
|
|
35
63
|
afterAll(() => {
|
|
36
64
|
process.env = OLD_ENV;
|
|
37
65
|
});
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
describe('initialization', () => {
|
|
68
|
+
it('initializes k8s client and baseUrl from kubeconfig', () => {
|
|
69
|
+
const svc = new KcpKubernetesService();
|
|
70
|
+
expect(svc.getKcpK8sCustomObjectsApiOIDCUser()).toBeDefined();
|
|
71
|
+
expect(svc.getKcpWorkspaceUrl('org1', 'acc1').toString()).toBe(
|
|
72
|
+
'https://kcp.example.com/clusters/root:orgs:org1:acc1',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
it('calls kubeconfig methods for OIDC user setup', () => {
|
|
77
|
+
new KcpKubernetesService();
|
|
78
|
+
expect(mockLoadFromFile).toHaveBeenCalledWith('/tmp/kcp.kubeconfig');
|
|
79
|
+
expect(mockAddUser).toHaveBeenCalledWith({ name: 'oidc' });
|
|
80
|
+
expect(mockAddContext).toHaveBeenCalledWith({
|
|
81
|
+
name: 'oidc',
|
|
82
|
+
user: 'oidc',
|
|
83
|
+
cluster: 'test-cluster',
|
|
84
|
+
});
|
|
85
|
+
expect(mockSetCurrentContext).toHaveBeenCalledWith('oidc');
|
|
86
|
+
});
|
|
53
87
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
88
|
+
it('creates multiple API clients', () => {
|
|
89
|
+
new KcpKubernetesService();
|
|
90
|
+
expect(mockMakeApiClient).toHaveBeenCalledTimes(3);
|
|
91
|
+
});
|
|
59
92
|
});
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
describe('getters', () => {
|
|
95
|
+
it('returns custom objects API for OIDC user', () => {
|
|
96
|
+
const svc = new KcpKubernetesService();
|
|
97
|
+
const api = svc.getKcpK8sCustomObjectsApiOIDCUser();
|
|
98
|
+
expect(api).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns custom objects API', () => {
|
|
102
|
+
const svc = new KcpKubernetesService();
|
|
103
|
+
const api = svc.getKcpK8sCustomObjectsApi();
|
|
104
|
+
expect(api).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns core v1 API', () => {
|
|
108
|
+
const svc = new KcpKubernetesService();
|
|
109
|
+
const api = svc.getKcpK8sCoreV1Api();
|
|
110
|
+
expect(api).toBeDefined();
|
|
111
|
+
});
|
|
66
112
|
});
|
|
67
113
|
|
|
68
|
-
describe('
|
|
69
|
-
|
|
114
|
+
describe('workspace URL building', () => {
|
|
115
|
+
it('builds workspace url with organization and account', () => {
|
|
116
|
+
const svc = new KcpKubernetesService();
|
|
117
|
+
expect(svc.getKcpWorkspaceUrl('org1', 'acc1').toString()).toBe(
|
|
118
|
+
'https://kcp.example.com/clusters/root:orgs:org1:acc1',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
122
|
+
it('builds workspace url without account', () => {
|
|
123
|
+
const svc = new KcpKubernetesService();
|
|
124
|
+
expect(svc.getKcpWorkspaceUrl('org1', '').toString()).toBe(
|
|
125
|
+
'https://kcp.example.com/clusters/root:orgs:org1',
|
|
126
|
+
);
|
|
76
127
|
});
|
|
77
128
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
129
|
+
it('builds workspace url without organization', () => {
|
|
130
|
+
const svc = new KcpKubernetesService();
|
|
131
|
+
expect(svc.getKcpWorkspaceUrl('', '').toString()).toBe(
|
|
132
|
+
'https://kcp.example.com/clusters/root:orgs',
|
|
133
|
+
);
|
|
81
134
|
});
|
|
82
135
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const addContext = jest.fn();
|
|
88
|
-
const setCurrentContext = jest.fn();
|
|
89
|
-
const getCurrentCluster = jest.fn(
|
|
90
|
-
() => ({ name: 'c', server: 'https://kcp.internal' }) as any,
|
|
136
|
+
it('builds workspace url with only organization', () => {
|
|
137
|
+
const svc = new KcpKubernetesService();
|
|
138
|
+
expect(svc.getKcpWorkspaceUrl('orgX').toString()).toBe(
|
|
139
|
+
'https://kcp.example.com/clusters/root:orgs:orgX',
|
|
91
140
|
);
|
|
92
|
-
|
|
141
|
+
});
|
|
93
142
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
getCurrentCluster,
|
|
102
|
-
makeApiClient,
|
|
103
|
-
})),
|
|
104
|
-
CustomObjectsApi: jest.fn(),
|
|
105
|
-
};
|
|
106
|
-
});
|
|
143
|
+
it('builds workspace url without parameters', () => {
|
|
144
|
+
const svc = new KcpKubernetesService();
|
|
145
|
+
expect(svc.getKcpWorkspaceUrl().toString()).toBe(
|
|
146
|
+
'https://kcp.example.com/clusters/root:orgs',
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
107
150
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
151
|
+
describe('virtual workspace URL building', () => {
|
|
152
|
+
it('builds virtual workspace url with account', () => {
|
|
153
|
+
const svc = new KcpKubernetesService();
|
|
154
|
+
expect(svc.getKcpVirtualWorkspaceUrl('orgX', 'accY').toString()).toBe(
|
|
155
|
+
'https://kcp.example.com/services/contentconfigurations/clusters/root:orgs:orgX:accY',
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('builds virtual workspace url without account', () => {
|
|
160
|
+
const svc = new KcpKubernetesService();
|
|
161
|
+
expect(svc.getKcpVirtualWorkspaceUrl('orgX', '').toString()).toBe(
|
|
162
|
+
'https://kcp.example.com/services/contentconfigurations/clusters/root:orgs:orgX',
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
112
166
|
|
|
167
|
+
describe('getKcpWorkspacePublicUrl', () => {
|
|
113
168
|
const makeReq = (overrides: Partial<Request> = {}): Request =>
|
|
114
169
|
({
|
|
115
170
|
headers: {},
|
|
@@ -118,7 +173,7 @@ describe('KcpKubernetesService', () => {
|
|
|
118
173
|
}) as unknown as Request;
|
|
119
174
|
|
|
120
175
|
it('builds URL with organization and account from query', () => {
|
|
121
|
-
const svc =
|
|
176
|
+
const svc = new KcpKubernetesService();
|
|
122
177
|
const req = makeReq({
|
|
123
178
|
query: { 'core_platform-mesh_io_account': 'acc-1' } as any,
|
|
124
179
|
headers: { host: 'kcp.api.example.com' } as any,
|
|
@@ -130,18 +185,20 @@ describe('KcpKubernetesService', () => {
|
|
|
130
185
|
);
|
|
131
186
|
});
|
|
132
187
|
|
|
133
|
-
it('omits port for standard
|
|
134
|
-
const svc =
|
|
135
|
-
|
|
136
|
-
let req = makeReq({
|
|
188
|
+
it('omits port for standard port 80', () => {
|
|
189
|
+
const svc = new KcpKubernetesService();
|
|
190
|
+
const req = makeReq({
|
|
137
191
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
138
192
|
headers: { host: 'kcp.api.example.com:80' } as any,
|
|
139
193
|
});
|
|
140
194
|
expect(svc.getKcpWorkspacePublicUrl(req)).toBe(
|
|
141
195
|
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc',
|
|
142
196
|
);
|
|
197
|
+
});
|
|
143
198
|
|
|
144
|
-
|
|
199
|
+
it('omits port for standard port 443', () => {
|
|
200
|
+
const svc = new KcpKubernetesService();
|
|
201
|
+
const req = makeReq({
|
|
145
202
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
146
203
|
headers: {
|
|
147
204
|
'x-forwarded-port': '443',
|
|
@@ -151,8 +208,11 @@ describe('KcpKubernetesService', () => {
|
|
|
151
208
|
expect(svc.getKcpWorkspacePublicUrl(req)).toBe(
|
|
152
209
|
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc',
|
|
153
210
|
);
|
|
211
|
+
});
|
|
154
212
|
|
|
155
|
-
|
|
213
|
+
it('omits port when no port provided', () => {
|
|
214
|
+
const svc = new KcpKubernetesService();
|
|
215
|
+
const req = makeReq({
|
|
156
216
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
157
217
|
headers: { host: 'kcp.api.example.com' } as any,
|
|
158
218
|
});
|
|
@@ -162,7 +222,7 @@ describe('KcpKubernetesService', () => {
|
|
|
162
222
|
});
|
|
163
223
|
|
|
164
224
|
it('appends non-standard port from x-forwarded-port', () => {
|
|
165
|
-
const svc =
|
|
225
|
+
const svc = new KcpKubernetesService();
|
|
166
226
|
const req = makeReq({
|
|
167
227
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
168
228
|
headers: {
|
|
@@ -177,8 +237,24 @@ describe('KcpKubernetesService', () => {
|
|
|
177
237
|
);
|
|
178
238
|
});
|
|
179
239
|
|
|
240
|
+
it('handles x-forwarded-port as array', () => {
|
|
241
|
+
const svc = new KcpKubernetesService();
|
|
242
|
+
const req = makeReq({
|
|
243
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
244
|
+
headers: {
|
|
245
|
+
'x-forwarded-port': ['9000', '8000'],
|
|
246
|
+
host: 'kcp.api.example.com',
|
|
247
|
+
} as any,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
251
|
+
expect(url).toBe(
|
|
252
|
+
'https://kcp.api.example.com:9000/clusters/root:orgs:org-1:acc',
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
180
256
|
it('falls back to port from host header when x-forwarded-port not present', () => {
|
|
181
|
-
const svc =
|
|
257
|
+
const svc = new KcpKubernetesService();
|
|
182
258
|
const req = makeReq({
|
|
183
259
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
184
260
|
headers: { host: 'kcp.api.example.com:3000' } as any,
|
|
@@ -192,7 +268,7 @@ describe('KcpKubernetesService', () => {
|
|
|
192
268
|
|
|
193
269
|
it('uses FRONTEND_PORT env when provided', () => {
|
|
194
270
|
process.env.FRONTEND_PORT = '4200';
|
|
195
|
-
const svc =
|
|
271
|
+
const svc = new KcpKubernetesService();
|
|
196
272
|
const req = makeReq({
|
|
197
273
|
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
198
274
|
headers: { host: 'kcp.api.example.com' } as any,
|
|
@@ -203,5 +279,342 @@ describe('KcpKubernetesService', () => {
|
|
|
203
279
|
'https://kcp.api.example.com:4200/clusters/root:orgs:org-1:acc',
|
|
204
280
|
);
|
|
205
281
|
});
|
|
282
|
+
|
|
283
|
+
it('prioritizes FRONTEND_PORT over x-forwarded-port', () => {
|
|
284
|
+
process.env.FRONTEND_PORT = '5000';
|
|
285
|
+
const svc = new KcpKubernetesService();
|
|
286
|
+
const req = makeReq({
|
|
287
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
288
|
+
headers: {
|
|
289
|
+
'x-forwarded-port': '8080',
|
|
290
|
+
host: 'kcp.api.example.com',
|
|
291
|
+
} as any,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
295
|
+
expect(url).toBe(
|
|
296
|
+
'https://kcp.api.example.com:5000/clusters/root:orgs:org-1:acc',
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('builds URL without account when not in query', () => {
|
|
301
|
+
const svc = new KcpKubernetesService();
|
|
302
|
+
const req = makeReq({
|
|
303
|
+
query: {} as any,
|
|
304
|
+
headers: { host: 'kcp.api.example.com' } as any,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
308
|
+
expect(url).toBe('https://kcp.api.example.com/clusters/root:orgs:org-1');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('listClusterCustomObject', () => {
|
|
313
|
+
it('calls API with correct workspace URL and parameters', async () => {
|
|
314
|
+
const svc = new KcpKubernetesService();
|
|
315
|
+
const gvr: K8sResourceDescriptor = {
|
|
316
|
+
group: 'apps',
|
|
317
|
+
version: 'v1',
|
|
318
|
+
plural: 'deployments',
|
|
319
|
+
name: 'my-deployment',
|
|
320
|
+
};
|
|
321
|
+
const context: K8sRequestContext = {
|
|
322
|
+
organization: 'test-org',
|
|
323
|
+
'core_platform-mesh_io_account': 'test-account',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
mockListClusterCustomObject.mockResolvedValue({ data: {} });
|
|
327
|
+
|
|
328
|
+
await svc.listClusterCustomObject(gvr, context);
|
|
329
|
+
|
|
330
|
+
expect(mockListClusterCustomObject).toHaveBeenCalledWith(
|
|
331
|
+
gvr,
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
middleware: expect.any(Array),
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('builds correct URL path in middleware', async () => {
|
|
339
|
+
const svc = new KcpKubernetesService();
|
|
340
|
+
const gvr: K8sResourceDescriptor = {
|
|
341
|
+
group: 'batch',
|
|
342
|
+
version: 'v1',
|
|
343
|
+
plural: 'jobs',
|
|
344
|
+
name: 'test-job',
|
|
345
|
+
};
|
|
346
|
+
const context: K8sRequestContext = {
|
|
347
|
+
organization: 'org1',
|
|
348
|
+
'core_platform-mesh_io_account': 'acc1',
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
let capturedContext: any;
|
|
352
|
+
mockListClusterCustomObject.mockImplementation(
|
|
353
|
+
async (gvrParam, options) => {
|
|
354
|
+
const middleware = options.middleware[0];
|
|
355
|
+
const mockContext = {
|
|
356
|
+
setUrl: jest.fn(),
|
|
357
|
+
};
|
|
358
|
+
capturedContext = mockContext;
|
|
359
|
+
await middleware.options.pre(mockContext);
|
|
360
|
+
return { data: {} };
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await svc.listClusterCustomObject(gvr, context);
|
|
365
|
+
|
|
366
|
+
expect(capturedContext.setUrl).toHaveBeenCalledWith(
|
|
367
|
+
'https://kcp.example.com/clusters/root:orgs:org1:acc1/apis/batch/v1/jobs/test-job',
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('handles context without account', async () => {
|
|
372
|
+
const svc = new KcpKubernetesService();
|
|
373
|
+
const gvr: K8sResourceDescriptor = {
|
|
374
|
+
group: 'core',
|
|
375
|
+
version: 'v1',
|
|
376
|
+
plural: 'pods',
|
|
377
|
+
name: 'my-pod',
|
|
378
|
+
};
|
|
379
|
+
const context: K8sRequestContext = {
|
|
380
|
+
organization: 'org2',
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
let capturedContext: any;
|
|
384
|
+
mockListClusterCustomObject.mockImplementation(
|
|
385
|
+
async (gvrParam, options) => {
|
|
386
|
+
const middleware = options.middleware[0];
|
|
387
|
+
const mockContext = {
|
|
388
|
+
setUrl: jest.fn(),
|
|
389
|
+
};
|
|
390
|
+
capturedContext = mockContext;
|
|
391
|
+
await middleware.options.pre(mockContext);
|
|
392
|
+
return { data: {} };
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
await svc.listClusterCustomObject(gvr, context);
|
|
397
|
+
|
|
398
|
+
expect(capturedContext.setUrl).toHaveBeenCalledWith(
|
|
399
|
+
'https://kcp.example.com/clusters/root:orgs:org2/apis/core/v1/pods/my-pod',
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe('listClusterCustomObjectInKcpVirtualWorkspace', () => {
|
|
405
|
+
it('calls OIDC API with correct virtual workspace URL and token', async () => {
|
|
406
|
+
const svc = new KcpKubernetesService();
|
|
407
|
+
const gvr: K8sResourceDescriptor = {
|
|
408
|
+
group: 'networking.k8s.io',
|
|
409
|
+
version: 'v1',
|
|
410
|
+
plural: 'ingresses',
|
|
411
|
+
name: undefined,
|
|
412
|
+
};
|
|
413
|
+
const context: K8sRequestContext = {
|
|
414
|
+
organization: 'virtual-org',
|
|
415
|
+
'core_platform-mesh_io_account': 'virtual-acc',
|
|
416
|
+
};
|
|
417
|
+
const token = 'test-bearer-token';
|
|
418
|
+
|
|
419
|
+
mockListClusterCustomObject.mockResolvedValue({ data: [] });
|
|
420
|
+
|
|
421
|
+
await svc.listClusterCustomObjectInKcpVirtualWorkspace(
|
|
422
|
+
gvr,
|
|
423
|
+
context,
|
|
424
|
+
token,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
expect(mockListClusterCustomObject).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('sets Authorization header with Bearer token', async () => {
|
|
431
|
+
const svc = new KcpKubernetesService();
|
|
432
|
+
const gvr: K8sResourceDescriptor = {
|
|
433
|
+
group: 'apps',
|
|
434
|
+
version: 'v1',
|
|
435
|
+
plural: 'statefulsets',
|
|
436
|
+
name: undefined,
|
|
437
|
+
};
|
|
438
|
+
const context: K8sRequestContext = {
|
|
439
|
+
organization: 'auth-org',
|
|
440
|
+
'core_platform-mesh_io_account': 'auth-acc',
|
|
441
|
+
};
|
|
442
|
+
const token = 'my-secret-token';
|
|
443
|
+
|
|
444
|
+
let capturedContext: any;
|
|
445
|
+
mockListClusterCustomObject.mockImplementation(
|
|
446
|
+
async (gvrParam, options) => {
|
|
447
|
+
const middleware = options.middleware[0];
|
|
448
|
+
const mockContext = {
|
|
449
|
+
setUrl: jest.fn(),
|
|
450
|
+
setHeaderParam: jest.fn(),
|
|
451
|
+
};
|
|
452
|
+
capturedContext = mockContext;
|
|
453
|
+
await middleware.options.pre(mockContext);
|
|
454
|
+
return { data: [] };
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
await svc.listClusterCustomObjectInKcpVirtualWorkspace(
|
|
459
|
+
gvr,
|
|
460
|
+
context,
|
|
461
|
+
token,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(capturedContext.setHeaderParam).toHaveBeenCalledWith(
|
|
465
|
+
'Authorization',
|
|
466
|
+
'Bearer my-secret-token',
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('builds correct virtual workspace URL path', async () => {
|
|
471
|
+
const svc = new KcpKubernetesService();
|
|
472
|
+
const gvr: K8sResourceDescriptor = {
|
|
473
|
+
group: 'rbac.authorization.k8s.io',
|
|
474
|
+
version: 'v1',
|
|
475
|
+
plural: 'roles',
|
|
476
|
+
name: undefined,
|
|
477
|
+
};
|
|
478
|
+
const context: K8sRequestContext = {
|
|
479
|
+
organization: 'rbac-org',
|
|
480
|
+
'core_platform-mesh_io_account': 'rbac-acc',
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
let capturedContext: any;
|
|
484
|
+
mockListClusterCustomObject.mockImplementation(
|
|
485
|
+
async (gvrParam, options) => {
|
|
486
|
+
const middleware = options.middleware[0];
|
|
487
|
+
const mockContext = {
|
|
488
|
+
setUrl: jest.fn(),
|
|
489
|
+
setHeaderParam: jest.fn(),
|
|
490
|
+
};
|
|
491
|
+
capturedContext = mockContext;
|
|
492
|
+
await middleware.options.pre(mockContext);
|
|
493
|
+
return { data: [] };
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
await svc.listClusterCustomObjectInKcpVirtualWorkspace(
|
|
498
|
+
gvr,
|
|
499
|
+
context,
|
|
500
|
+
'token',
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(capturedContext.setUrl).toHaveBeenCalledWith(
|
|
504
|
+
'https://kcp.example.com/services/contentconfigurations/clusters/root:orgs:rbac-org:rbac-acc/apis/rbac.authorization.k8s.io/v1/roles',
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('getClientSecret', () => {
|
|
510
|
+
it('retrieves and decodes client secret successfully', async () => {
|
|
511
|
+
const svc = new KcpKubernetesService();
|
|
512
|
+
const orgName = 'test-org';
|
|
513
|
+
const encodedSecret = Buffer.from('my-secret-value').toString('base64');
|
|
514
|
+
|
|
515
|
+
mockReadNamespacedSecret.mockResolvedValue({
|
|
516
|
+
data: {
|
|
517
|
+
client_secret: encodedSecret,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const result = await svc.getClientSecret(orgName);
|
|
522
|
+
|
|
523
|
+
expect(result).toBe('my-secret-value');
|
|
524
|
+
expect(mockReadNamespacedSecret).toHaveBeenCalledWith(
|
|
525
|
+
{
|
|
526
|
+
namespace: 'default',
|
|
527
|
+
name: 'portal-client-secret-test-org-test-org',
|
|
528
|
+
},
|
|
529
|
+
expect.objectContaining({
|
|
530
|
+
middleware: expect.any(Array),
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('builds correct secret name and namespace', async () => {
|
|
536
|
+
const svc = new KcpKubernetesService();
|
|
537
|
+
const orgName = 'my-company';
|
|
538
|
+
|
|
539
|
+
mockReadNamespacedSecret.mockResolvedValue({
|
|
540
|
+
data: {
|
|
541
|
+
client_secret: Buffer.from('secret').toString('base64'),
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await svc.getClientSecret(orgName);
|
|
546
|
+
|
|
547
|
+
expect(mockReadNamespacedSecret).toHaveBeenCalledWith(
|
|
548
|
+
{
|
|
549
|
+
namespace: 'default',
|
|
550
|
+
name: 'portal-client-secret-my-company-my-company',
|
|
551
|
+
},
|
|
552
|
+
expect.any(Object),
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('uses correct workspace URL in middleware', async () => {
|
|
557
|
+
const svc = new KcpKubernetesService();
|
|
558
|
+
const orgName = 'url-org';
|
|
559
|
+
|
|
560
|
+
let capturedContext: any;
|
|
561
|
+
mockReadNamespacedSecret.mockImplementation(async (params, options) => {
|
|
562
|
+
const middleware = options.middleware[0];
|
|
563
|
+
const mockContext = {
|
|
564
|
+
setUrl: jest.fn(),
|
|
565
|
+
};
|
|
566
|
+
capturedContext = mockContext;
|
|
567
|
+
await middleware.options.pre(mockContext);
|
|
568
|
+
return {
|
|
569
|
+
data: {
|
|
570
|
+
client_secret: Buffer.from('test').toString('base64'),
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
await svc.getClientSecret(orgName);
|
|
576
|
+
|
|
577
|
+
expect(capturedContext.setUrl).toHaveBeenCalledWith(
|
|
578
|
+
'https://kcp.example.com/clusters/root:orgs/api/v1/namespaces/default/secrets/portal-client-secret-url-org-url-org',
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('throws error when secret retrieval fails', async () => {
|
|
583
|
+
const svc = new KcpKubernetesService();
|
|
584
|
+
const orgName = 'fail-org';
|
|
585
|
+
const error = new Error('Secret not found');
|
|
586
|
+
|
|
587
|
+
mockReadNamespacedSecret.mockRejectedValue(error);
|
|
588
|
+
|
|
589
|
+
await expect(svc.getClientSecret(orgName)).rejects.toThrow(
|
|
590
|
+
'Secret not found',
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('logs error with secret name when retrieval fails', async () => {
|
|
595
|
+
const svc = new KcpKubernetesService();
|
|
596
|
+
const orgName = 'error-org';
|
|
597
|
+
const error = {
|
|
598
|
+
response: {
|
|
599
|
+
body: { message: 'Not found' },
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
mockReadNamespacedSecret.mockRejectedValue(error);
|
|
604
|
+
|
|
605
|
+
await expect(svc.getClientSecret(orgName)).rejects.toEqual(error);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('handles error without response body', async () => {
|
|
609
|
+
const svc = new KcpKubernetesService();
|
|
610
|
+
const orgName = 'no-response-org';
|
|
611
|
+
const error = new Error('Network error');
|
|
612
|
+
|
|
613
|
+
mockReadNamespacedSecret.mockRejectedValue(error);
|
|
614
|
+
|
|
615
|
+
await expect(svc.getClientSecret(orgName)).rejects.toThrow(
|
|
616
|
+
'Network error',
|
|
617
|
+
);
|
|
618
|
+
});
|
|
206
619
|
});
|
|
207
620
|
});
|