Skip to content

Commit 8fb08a2

Browse files
mmilko01github-actions[bot]nikmace
authored
feat: Extend add-new-model generator to support external services for CF projects (#4523)
* feat: Extend add-new-model generator to support external services for CF projects * fix: various fixes * fix: add additional message for annotation URI prompt * Linting auto fix commit * fix: remove install and build steps from generator * refactor: remove unneeded code * chore: create changeset * chore: fix changeset * test: fix tests for windows * test: fix tests for windows * test: fix api tests file format * refactor: move btp related api call to /btp * fix: sonar issues * Linting auto fix commit * fix: address comments * fix: address text comments * fix: sonar issue * Linting auto fix commit * fix: expected texts in tests * fix: for HTTP service type use different change type * fix: address comments * chore: address text comments * fix: translations json * fix: lint issue * chore: simplify code * fix: address comments * test: update changed text * fix: validation for duplicate change * fix: add check for org/space mismatch * fix: remove leading slash in CF scenario for change uri * Linting auto fix commit --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Nikita B. <[email protected]>
1 parent 406be4f commit 8fb08a2

28 files changed

Lines changed: 1813 additions & 507 deletions

File tree

.changeset/selfish-monkeys-joke.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@sap-ux/adp-tooling": patch
3+
"@sap-ux/generator-adp": patch
4+
"@sap-ux/create": patch
5+
---
6+
7+
feat: Extend add-new-model generator to support external services for CF projects

packages/adp-tooling/src/btp/api.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import axios from 'axios';
22

33
import type { ToolsLogger } from '@sap-ux/logger';
4+
import type { Destinations } from '@sap-ux/btp-utils';
45

56
import { t } from '../i18n';
6-
import type { Uaa, BtpDestinationConfig } from '../types';
7+
import type { Uaa, BtpDestinationConfig, CfDestinationServiceCredentials } from '../types';
78

89
/**
910
* Obtain an OAuth2 access token using the client credentials grant.
@@ -27,8 +28,8 @@ export async function getToken(uaa: Uaa, logger?: ToolsLogger): Promise<string>
2728
logger?.debug('OAuth token obtained successfully');
2829
return response.data['access_token'];
2930
} catch (e) {
30-
logger?.error(`Failed to obtain OAuth token from ${uri}: ${e.message}`);
31-
throw new Error(t('error.failedToGetAuthKey', { error: e.message }));
31+
logger?.error(`Failed to obtain OAuth token from ${uri}: ${e instanceof Error ? e.message : String(e)}`);
32+
throw new Error(t('error.failedToGetAuthKey', { error: e instanceof Error ? e.message : String(e) }));
3233
}
3334
}
3435

@@ -59,7 +60,43 @@ export async function getBtpDestinationConfig(
5960
logger?.debug(`Destination "${destinationName}" config: ProxyType=${config?.ProxyType}`);
6061
return config;
6162
} catch (e) {
62-
logger?.error(`Failed to fetch destination config for "${destinationName}": ${e.message}`);
63+
logger?.error(
64+
`Failed to fetch destination config for "${destinationName}": ${e instanceof Error ? e.message : String(e)}`
65+
);
6366
return undefined;
6467
}
6568
}
69+
70+
/**
71+
* Lists all subaccount destinations from the BTP Destination Configuration API.
72+
*
73+
* @param {CfDestinationServiceCredentials} credentials - Destination service credentials.
74+
* @returns {Promise<Destinations>} Map of destination name to Destination object.
75+
*/
76+
export async function listBtpDestinations(credentials: CfDestinationServiceCredentials): Promise<Destinations> {
77+
const uaa =
78+
'uaa' in credentials
79+
? credentials.uaa
80+
: { clientid: credentials.clientid, clientsecret: credentials.clientsecret, url: credentials.url };
81+
const token = await getToken(uaa);
82+
const url = `${credentials.uri}/destination-configuration/v1/subaccountDestinations`;
83+
try {
84+
const response = await axios.get<BtpDestinationConfig[]>(url, {
85+
headers: { Authorization: `Bearer ${token}` }
86+
});
87+
const configs = Array.isArray(response.data) ? response.data : [];
88+
return configs.reduce<Destinations>((acc, config) => {
89+
acc[config.Name] = {
90+
Name: config.Name,
91+
Host: config.URL,
92+
Type: config.Type,
93+
Authentication: config.Authentication,
94+
ProxyType: config.ProxyType,
95+
Description: config.Description ?? ''
96+
};
97+
return acc;
98+
}, {});
99+
} catch (e) {
100+
throw new Error(t('error.failedToListBtpDestinations', { error: e instanceof Error ? e.message : String(e) }));
101+
}
102+
}

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
ServiceKeys
1717
} from '../../types';
1818
import { AppRouterType } from '../../types';
19-
import { createServices } from '../services/api';
19+
import { createServices, createServiceInstance, getOrCreateServiceInstanceKeys } from '../services/api';
2020
import { getProjectNameForXsSecurity, getYamlContent } from './yaml-loader';
2121
import { getServiceKeyDestinations } from '../app/discovery';
2222

@@ -44,6 +44,55 @@ export function isMtaProject(selectedPath: string): boolean {
4444
return fs.existsSync(path.join(selectedPath, 'mta.yaml'));
4545
}
4646

47+
/**
48+
* Adds a connectivity service resource to the project's mta.yaml if not already present,
49+
* creates the CF service instance and generates a service key for it.
50+
* Only applies to MTA projects. Required when the selected CF destination is OnPremise
51+
* so the AppRouter can proxy requests through the Cloud Connector.
52+
*
53+
* @param {string} projectPath - The root path of the project.
54+
* @param {Editor} memFs - The mem-fs editor instance.
55+
* @param {ToolsLogger} [logger] - Optional logger.
56+
*/
57+
export async function addConnectivityServiceToMta(
58+
projectPath: string,
59+
memFs: Editor,
60+
logger?: ToolsLogger
61+
): Promise<void> {
62+
if (!isMtaProject(projectPath)) {
63+
return;
64+
}
65+
66+
const mtaYamlPath = path.join(projectPath, 'mta.yaml');
67+
const yamlContent = getYamlContent<MtaYaml>(mtaYamlPath);
68+
if (!yamlContent) {
69+
return;
70+
}
71+
72+
const projectName = yamlContent.ID.toLowerCase();
73+
const connectivityResourceName = `${projectName}-connectivity`;
74+
75+
if (yamlContent.resources?.some((r: MtaResource) => r.name === connectivityResourceName)) {
76+
return;
77+
}
78+
79+
await createServiceInstance('lite', connectivityResourceName, 'connectivity', { logger });
80+
await getOrCreateServiceInstanceKeys({ names: [connectivityResourceName] }, logger);
81+
82+
yamlContent.resources = yamlContent.resources ?? [];
83+
yamlContent.resources.push({
84+
name: connectivityResourceName,
85+
type: CF_MANAGED_SERVICE,
86+
parameters: {
87+
service: 'connectivity',
88+
'service-plan': 'lite',
89+
'service-name': connectivityResourceName
90+
}
91+
});
92+
93+
memFs.write(mtaYamlPath, yaml.dump(yamlContent, { lineWidth: -1 }));
94+
}
95+
4796
/**
4897
* Gets the SAP Cloud Service.
4998
*
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as path from 'node:path';
2+
3+
import type { Destinations } from '@sap-ux/btp-utils';
4+
5+
import { getOrCreateServiceInstanceKeys } from './api';
6+
import { listBtpDestinations } from '../../btp/api';
7+
import { getYamlContent } from '../project/yaml-loader';
8+
import { t } from '../../i18n';
9+
import type { CfDestinationServiceCredentials, MtaYaml } from '../../types';
10+
11+
/**
12+
* Finds the name of the destination service instance declared in the MTA project's mta.yaml.
13+
*
14+
* @param {string} projectPath - The root path of the app project.
15+
* @returns {string} The CF service instance name.
16+
* @throws {Error} When the destination service instance is not found or mta.yaml cannot be read.
17+
*/
18+
function getDestinationServiceName(projectPath: string): string {
19+
try {
20+
const yamlContent = getYamlContent<MtaYaml>(path.join(path.dirname(projectPath), 'mta.yaml'));
21+
const name = yamlContent?.resources?.find((r) => r.parameters?.service === 'destination')?.name;
22+
if (!name) {
23+
throw new Error(t('error.destinationServiceNotFoundInMtaYaml'));
24+
}
25+
return name;
26+
} catch (e) {
27+
throw e instanceof Error ? e : new Error(t('error.destinationServiceNotFoundInMtaYaml'));
28+
}
29+
}
30+
31+
/**
32+
* Returns the list of available BTP destinations from the logged-in CF subaccount.
33+
* Reads the destination service credentials from the CF project's service keys
34+
* and calls the BTP Destination Configuration API directly.
35+
*
36+
* @param {string} projectPath - The root path of the CF app project.
37+
* @returns {Promise<Destinations>} Map of destination name to Destination object.
38+
*/
39+
export async function getBtpDestinations(projectPath: string): Promise<Destinations> {
40+
const destinationServiceName = getDestinationServiceName(projectPath);
41+
42+
const serviceInfo = await getOrCreateServiceInstanceKeys({ names: [destinationServiceName] });
43+
if (!serviceInfo?.serviceKeys?.length) {
44+
throw new Error(t('error.noServiceKeysFoundForDestination', { serviceInstanceName: destinationServiceName }));
45+
}
46+
47+
const credentials = serviceInfo.serviceKeys[0].credentials as CfDestinationServiceCredentials;
48+
return listBtpDestinations(credentials);
49+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './api';
22
export * from './ssh';
33
export * from './cli';
4+
export * from './destinations';
45
export * from './manifest';

0 commit comments

Comments
 (0)