Skip to content

Commit 6b74074

Browse files
feat: Change backend-proxy-middleware-cf to use approuter instead of token exchange (#4160)
* feat: add approuter support * refactor: code improvements * refactor: interfaces and jsdocs * refactor: code and split methods * refactor: move types * chore: add cset * refactor: move types * refactor: set default env options in process * refactor: code improvements * refactor: middleware options * test: add test suites * test: add test suites * fix: sonar issues * refactor: improve code quality and tests * test: add test case * fix: sonar issue with regxp * feat: add service cred retrieval for approuter env options * test: add new tooling tests * test: add missing test cases * fix: sonar issues * fix: dest merging from env path * fix: review comments * feat: add approuter lazy loading, dynamic port allocation, loop prevention mechanism * fix: sonar issue * refactor: move interfaces * refactor: code improvements * feat: add needed configurations for bas * refactor: clear text * feat: update xsuaa service runtime * fix: incorrect credentials object return for vcap services * fix: add error handling for approuter start * Linting auto fix commit * fix: add wait flag for cf cli when updating service * test: fix failing test * chore: update cset * chore: update cset * feat: add default path for xsapp.json * fix: test noise and missing api call mock * fix: remove unused var from test * refactor: change folder structure in the middleware * fix: forward error if approuter instantiation fails * fix: use object assign for process env vars * refactor: routes, emit warning when route is skipped * fix: escape regex metacharacters in custom route path filter * chore: update readme with title links --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 90aa468 commit 6b74074

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3967
-1941
lines changed

.changeset/modern-canyons-wear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@sap-ux/backend-proxy-middleware-cf': minor
3+
'@sap-ux/adp-tooling': patch
4+
---
5+
6+
feat: Change `backend-proxy-middleware-cf` to use `approuter` instead of token exchange

.vscode/launch.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"--extensionDevelopmentPath=${workspaceFolder}/packages/abap-deploy-config-sub-generator",
2626
"--extensionDevelopmentPath=${workspaceFolder}/packages/abap-deploy-config-inquirer",
2727
"--extensionDevelopmentPath=${workspaceFolder}/packages/deploy-config-sub-generator",
28+
"--extensionDevelopmentPath=${workspaceFolder}/packages/backend-proxy-middleware-cf",
2829
"--extensionDevelopmentPath=${workspaceFolder}/packages/ui5-application-inquirer",
2930
"--extensionDevelopmentPath=${workspaceFolder}/packages/ui5-info",
3031
"--extensionDevelopmentPath=${workspaceFolder}/packages/ui5-config"
@@ -39,6 +40,7 @@
3940
"${workspaceRoot}/packages/abap-deploy-config-sub-generator/generators/**/*.js",
4041
"${workspaceRoot}/packages/abap-deploy-config-inquirer/dist/**/*.js",
4142
"${workspaceRoot}/packages/deploy-config-sub-generator/generators/**/*.js",
43+
"${workspaceRoot}/packages/backend-proxy-middleware-cf/dist/**/*.js",
4244
"${workspaceRoot}/packages/ui5-application-inquirer/dist/**/*.js",
4345
"${workspaceRoot}/packages/ui5-info/dist/**/*.js",
4446
"${workspaceRoot}/packages/ui5-config/dist/**/*.js"

packages/adp-tooling/src/base/helper.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ReaderCollection } from '@ui5/fs'; // eslint-disable-line sonarjs/
33
import { existsSync, readdirSync, readFileSync } from 'node:fs';
44
import { join, isAbsolute, relative, basename, dirname } from 'node:path';
55

6+
import type { ToolsLogger } from '@sap-ux/logger';
67
import type { UI5Config } from '@sap-ux/ui5-config';
78
import { type InboundContent, type Inbound, AdaptationProjectType } from '@sap-ux/axios-extension';
89
import {
@@ -113,6 +114,24 @@ export function extractCfBuildTask(ui5Conf: UI5Config): UI5YamlCustomTaskConfigu
113114
return buildTask;
114115
}
115116

117+
/**
118+
* Read space GUID from ui5.yaml customTasks app-variant-bundler-build.space.
119+
*
120+
* @param {string} rootPath - Project root (where ui5.yaml lives).
121+
* @param {ToolsLogger} logger - Optional logger.
122+
* @returns {Promise<string | undefined>} Space GUID or undefined if not found.
123+
*/
124+
export async function getSpaceGuidFromUi5Yaml(rootPath: string, logger?: ToolsLogger): Promise<string | undefined> {
125+
try {
126+
const ui5Config = await readUi5Config(rootPath, 'ui5.yaml');
127+
const buildTask = extractCfBuildTask(ui5Config);
128+
return buildTask?.space;
129+
} catch {
130+
logger?.warn('Could not read space from ui5.yaml (app-variant-bundler-build).');
131+
return undefined;
132+
}
133+
}
134+
116135
/**
117136
* Read the manifest from the build output folder.
118137
*
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1+
export * from './mta';
12
export * from './yaml';
23
export * from './yaml-loader';
3-
export * from './mta';

packages/adp-tooling/src/cf/project/mta.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,54 @@ import * as path from 'node:path';
33
import type { ToolsLogger } from '@sap-ux/logger';
44

55
import { t } from '../../i18n';
6+
import type { CfServiceOffering, CfAPIResponse, BusinessServiceResource, AppRouterType, MtaYaml } from '../../types';
7+
import { getServiceKeyCredentialsWithTags } from '../services/api';
8+
import { requestCfApi } from '../services/cli';
69
import { getRouterType } from './yaml';
710
import { getYamlContent } from './yaml-loader';
8-
import { requestCfApi } from '../services/cli';
9-
import type { CfServiceOffering, CfAPIResponse, BusinessServiceResource, AppRouterType } from '../../types';
11+
12+
const EXCLUDED_SERVICES_VCAP = new Set(['html5-apps-repo', 'portal']);
13+
14+
/**
15+
* Builds VCAP_SERVICES by resolving MTA resources to service key credentials.
16+
*
17+
* @param {MtaYaml['resources']} resources - MTA YAML resources.
18+
* @param {string} spaceGuid - The space GUID.
19+
* @param {ToolsLogger} logger - Optional logger.
20+
* @returns {Promise<Record<string, unknown>>} VCAP_SERVICES keyed by service name.
21+
*/
22+
export async function buildVcapServicesFromResources(
23+
resources: MtaYaml['resources'],
24+
spaceGuid: string,
25+
logger?: ToolsLogger
26+
): Promise<Record<string, unknown>> {
27+
const vcapServices: Record<string, unknown> = {};
28+
for (const resource of resources ?? []) {
29+
const serviceName = resource.parameters?.service;
30+
const serviceInstanceName = resource.parameters?.['service-name'];
31+
const servicePlan = resource.parameters?.['service-plan'];
32+
33+
if (!serviceName || !serviceInstanceName || EXCLUDED_SERVICES_VCAP.has(serviceName)) {
34+
continue;
35+
}
36+
37+
const data = await getServiceKeyCredentialsWithTags(
38+
spaceGuid,
39+
serviceName,
40+
serviceInstanceName,
41+
servicePlan ?? '',
42+
logger
43+
);
44+
45+
if (!data?.credentials) {
46+
throw new Error(`Credentials and tags for service '${serviceName}' ('${serviceInstanceName}') not found`);
47+
}
48+
49+
vcapServices[serviceName] = [data];
50+
}
51+
52+
return vcapServices;
53+
}
1054

1155
/**
1256
* Get the approuter type.

packages/adp-tooling/src/cf/services/api.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import type {
2020
CfServiceInstance,
2121
MtaYaml,
2222
ServiceInfo,
23-
CfUi5AppInfo
23+
CfUi5AppInfo,
24+
ServiceKeyCredentialsWithTags
2425
} from '../../types';
2526
import { t } from '../../i18n';
2627
import { getProjectNameForXsSecurity } from '../project';
@@ -437,3 +438,65 @@ export async function getOrCreateServiceKeys(
437438
throw new Error(t('error.failedToGetOrCreateServiceKeys', { serviceInstanceName, error: e.message }));
438439
}
439440
}
441+
442+
/**
443+
* Gets service tags for a given service name.
444+
*
445+
* @param {string} spaceGuid - The space GUID.
446+
* @param {string} serviceName - The service name (e.g., 'xsuaa', 'hana').
447+
* @returns {Promise<string[]>} The service tags.
448+
*/
449+
export async function getServiceTags(spaceGuid: string, serviceName: string): Promise<string[]> {
450+
const json: CfAPIResponse<CfServiceOffering> = await requestCfApi<CfAPIResponse<CfServiceOffering>>(
451+
`/v3/service_offerings?per_page=1000&space_guids=${spaceGuid}&names=${serviceName}`
452+
);
453+
const serviceOffering = json?.resources?.find((resource: CfServiceOffering) => resource.name === serviceName);
454+
return serviceOffering?.tags ?? [];
455+
}
456+
457+
/**
458+
* Fetches service tags and credentials for a single app-router resource (xsuaa/destination).
459+
*
460+
* @param {string} spaceGuid - The space GUID.
461+
* @param {string} serviceName - The service name (e.g. 'xsuaa', 'destination').
462+
* @param {string} serviceInstanceName - The service instance name.
463+
* @param {string} plan - The service plan.
464+
* @param {ToolsLogger} logger - Optional logger.
465+
* @returns {Promise<ServiceKeyCredentialsWithTags | null>} Service key credentials with tags returned by the CF API.
466+
*/
467+
export async function getServiceKeyCredentialsWithTags(
468+
spaceGuid: string,
469+
serviceName: string,
470+
serviceInstanceName: string,
471+
plan: string,
472+
logger?: ToolsLogger
473+
): Promise<ServiceKeyCredentialsWithTags | null> {
474+
try {
475+
const tags = await getServiceTags(spaceGuid, serviceName);
476+
477+
const serviceInstances = await getServiceInstance({
478+
names: [serviceInstanceName],
479+
spaceGuids: [spaceGuid]
480+
});
481+
482+
if (serviceInstances.length === 0) {
483+
logger?.error(`Service instance '${serviceInstanceName}' not found, skipping`);
484+
return null;
485+
}
486+
487+
const credentials = await getOrCreateServiceKeys(serviceInstances[0], logger);
488+
489+
return {
490+
label: serviceName,
491+
name: serviceInstanceName,
492+
tags,
493+
plan,
494+
credentials: credentials?.[0].credentials
495+
};
496+
} catch (e) {
497+
logger?.error(
498+
`Failed to get credentials and tags for service '${serviceName}' (instance: '${serviceInstanceName}'): ${e.message}`
499+
);
500+
return null;
501+
}
502+
}

packages/adp-tooling/src/cf/services/cli.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ export async function createServiceKey(serviceInstanceName: string, serviceKeyNa
106106
}
107107
}
108108

109+
/**
110+
* Updates a Cloud Foundry service instance with the given parameters.
111+
*
112+
* @param {string} serviceInstanceName - The service instance name.
113+
* @param {object} parameters - The configuration parameters to update.
114+
*/
115+
export async function updateServiceInstance(serviceInstanceName: string, parameters: object): Promise<void> {
116+
try {
117+
const cliResult = await Cli.execute(
118+
['update-service', serviceInstanceName, '-c', JSON.stringify(parameters), '--wait'],
119+
ENV
120+
);
121+
if (cliResult.exitCode !== 0) {
122+
throw new Error(cliResult.stderr);
123+
}
124+
} catch (e) {
125+
throw new Error(t('error.failedToUpdateServiceInstance', { serviceInstanceName, error: e.message }));
126+
}
127+
}
128+
109129
/**
110130
* Request CF API.
111131
*

packages/adp-tooling/src/translations/adp-tooling.i18n.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"emptyCFAPIResponse": "Empty response from CF API. Please verify your CF login status with 'cf target' and re-authenticate if needed with 'cf login'.",
104104
"xsSecurityJsonCouldNotBeParsed": "The xs-security.json file could not be parsed. Ensure the xs-security.json file exists and try again.",
105105
"failedToCreateServiceInstance": "Failed to create the service instance: '{{serviceInstanceName}}'. Error: {{error}}",
106+
"failedToUpdateServiceInstance": "Failed to update the service instance: '{{serviceInstanceName}}'. Error: {{error}}",
106107
"failedToGetFDCApps": "Retrieving FDC apps failed: {{error}}",
107108
"failedToGetFDCInbounds": "Retrieving inbounds from the UI5 Flexibility Design and Configuration service failed: {{error}}",
108109
"failedToConnectToFDCService": "Failed to connect to the FDC service: '{{status}}'",

packages/adp-tooling/src/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,22 @@ export interface ServiceInstance {
932932
guid: string;
933933
}
934934

935+
/**
936+
* Service key credentials with tags returned by the CF API.
937+
*/
938+
export interface ServiceKeyCredentialsWithTags {
939+
label: string;
940+
name: string;
941+
tags: string[];
942+
plan: string;
943+
credentials: ServiceKeys['credentials'] | undefined;
944+
}
945+
946+
export interface AppRouterEnvOptions {
947+
'VCAP_SERVICES'?: Record<string, unknown>;
948+
destinations?: { name: string; url: string }[];
949+
}
950+
935951
export interface GetServiceInstanceParams {
936952
spaceGuids?: string[];
937953
planNames?: string[];

packages/adp-tooling/test/unit/base/helper.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
filterAndMapInboundsToManifest,
1919
readUi5Config,
2020
extractCfBuildTask,
21+
getSpaceGuidFromUi5Yaml,
2122
readManifestFromBuildPath,
2223
loadAppVariant,
2324
getBaseAppId,
@@ -338,6 +339,37 @@ describe('helper', () => {
338339
});
339340
});
340341

342+
describe('getSpaceGuidFromUi5Yaml', () => {
343+
const rootPath = join(__dirname, '../../fixtures', 'adaptation-project');
344+
345+
beforeEach(() => {
346+
jest.clearAllMocks();
347+
});
348+
349+
test('returns space GUID when ui5.yaml has app-variant-bundler-build space', async () => {
350+
const spaceGuid = 'my-space-guid-123';
351+
const mockBuildTask = { space: spaceGuid };
352+
readUi5YamlMock.mockResolvedValue({
353+
findCustomTask: jest.fn().mockReturnValue({ configuration: mockBuildTask })
354+
} as unknown as UI5Config);
355+
356+
const result = await getSpaceGuidFromUi5Yaml(rootPath);
357+
358+
expect(readUi5YamlMock).toHaveBeenCalledWith(rootPath, 'ui5.yaml');
359+
expect(result).toBe(spaceGuid);
360+
});
361+
362+
test('returns undefined and calls logger.warn when space cannot be read', async () => {
363+
readUi5YamlMock.mockRejectedValue(new Error('File not found'));
364+
const logger = { warn: jest.fn() };
365+
366+
const result = await getSpaceGuidFromUi5Yaml(rootPath, logger as never);
367+
368+
expect(result).toBeUndefined();
369+
expect(logger.warn).toHaveBeenCalledWith('Could not read space from ui5.yaml (app-variant-bundler-build).');
370+
});
371+
});
372+
341373
describe('extractCfBuildTask', () => {
342374
beforeEach(() => {
343375
jest.clearAllMocks();

0 commit comments

Comments
 (0)