@ui5/task-adaptation
Version:
Custom task for ui5-builder which allows building UI5 Flexibility Adaptation Projects for SAP BTP, Cloud Foundry environment
175 lines • 8.37 kB
JavaScript
import { cfCreateService, cfGetInstanceCredentials } from "@sap/cf-tools/out/src/cf-local.js";
import { getSpaceGuidThrowIfUndefined } from "@sap/cf-tools/out/src/utils.js";
import { Cli } from "@sap/cf-tools/out/src/cli.js";
import { eFilters } from "@sap/cf-tools/out/src/types.js";
import { getLogger } from "@ui5/logger";
const log = getLogger("@ui5/task-adaptation::CFUtil");
export default class CFUtil {
/**
* Get or create service keys for service instance found by query
* @static
* @param {IGetServiceInstanceParams} getServiceInstanceParams query parameters to find a service instance by
* @param {ICreateServiceInstanceParams} [createServiceInstanceParams] parameters to create a service instance
* @return {Promise<IServiceKeys>} promise with service keys
* @memberof CFUtil
*/
static async getServiceInstanceKeys(getServiceInstanceParams, createServiceInstanceParams) {
let serviceInstances = await this.getServiceInstance(getServiceInstanceParams);
if (!(serviceInstances?.length > 0) && createServiceInstanceParams) {
await this.createService(createServiceInstanceParams);
serviceInstances = await this.getServiceInstance(getServiceInstanceParams);
}
if (!(serviceInstances?.length > 0)) {
throw new Error(`Cannot find '${getServiceInstanceParams.names?.join(", ")}' service in current space: ${getServiceInstanceParams.spaceGuids?.join(", ")}`);
}
// we can use any instance in the list to connect to HTML5 Repo
log.verbose(`Use '${serviceInstances[0].name}' HTML5 Repo Runtime service instance`);
const serviceKeys = await this.getOrCreateServiceKeys(serviceInstances[0]);
if (!(serviceKeys?.length > 0)) {
throw new Error(`Cannot get service keys for '${getServiceInstanceParams.names?.join(", ")}' service in current space: ${getServiceInstanceParams.spaceGuids?.join(", ")}`);
}
return {
credentials: serviceKeys[0].credentials,
serviceInstance: serviceInstances[0]
};
}
static async createService(params) {
log.verbose(`Creating a service instance with parameters: ${JSON.stringify(params)}`);
const serviceOfferings = await this.requestCfApi(`/v3/service_offerings?names=${params.serviceName}`);
if (serviceOfferings.length === 0) {
throw new Error(`Cannot find a service offering by name '${params.serviceName}'`);
}
const plans = await this.requestCfApi(`/v3/service_plans?service_offering_guids=${serviceOfferings[0].guid}`);
const plan = plans.find(plan => plan.name === params.planName);
if (!plan) {
throw new Error(`Cannot find a plan by name '${params.planName}' for service '${params.serviceName}'`);
}
try {
await cfCreateService(plan.guid, params.serviceInstanceName, params.parameters, params.tags);
}
catch (error) {
throw new Error(`Cannot create a service instance '${params.serviceInstanceName}' in space '${params.spaceGuid}': ${error.message}`);
}
}
static async getOrCreateServiceKeys(serviceInstance) {
const credentials = await this.getServiceKeys(serviceInstance.guid);
if (credentials.length === 0) {
const serviceKeyName = serviceInstance.name + "_key";
log.info(`Creating service key '${serviceKeyName}' for service instance '${serviceInstance.name}'`);
await this.createServiceKey(serviceInstance.name, serviceKeyName);
}
else {
return credentials;
}
return this.getServiceKeys(serviceInstance.guid);
}
static getServiceKeys(serviceInstanceGuid) {
return cfGetInstanceCredentials({
filters: [{
value: serviceInstanceGuid,
key: eFilters.service_instance_guids
}]
}).catch((error) => {
throw new Error("Failed to get service credentials: " + error.message);
});
}
static async createServiceKey(serviceInstanceName, serviceKeyName) {
try {
return this.cfExecute(["create-service-key", serviceInstanceName, serviceKeyName]);
}
catch (error) {
throw new Error(`Couldn't create a service key for instance: ${serviceInstanceName}: ${error}`);
}
}
static async getServiceInstance(params) {
const PARAM_MAP = {
spaceGuids: "space_guids",
planNames: "service_plan_names",
names: "names"
};
const parameters = Object.entries(params)
.filter(([, value]) => value?.length && value?.length > 0)
.map(([key, value]) => `${PARAM_MAP[key]}=${value?.join(",")}`);
const uri = `/v3/service_instances` + (parameters.length > 0 ? `?${parameters.join("&")}` : "");
const resources = await this.requestCfApi(uri);
return resources.map((service) => ({
name: service.name,
guid: service.guid
}));
}
static processErrors(json) {
if (json?.errors?.length > 0) {
const message = JSON.stringify(json.errors);
if (json?.errors?.some((e) => e.title === "CF-NotAuthenticated" || e.code === 10002)) {
throw new Error(`Authentication error. Use 'cf login' to authenticate in Cloud Foundry: ${message}`);
}
throw new Error(`Failed sending request to Cloud Foundry: ${message}`);
}
}
static async requestCfApi(url) {
const response = await this.cfExecute(["curl", url]);
const json = this.parseJson(response);
this.processErrors(json);
const resources = json?.resources;
const totalPages = json?.pagination?.total_pages;
if (totalPages > 1) {
const pages = Array.from({ length: totalPages - 1 }, (_, i) => i + 2);
return resources.concat(await Promise.all(pages.map(async (page) => {
const uri = `${url}${url.includes("?") ? "&" : "?"}page=${page}`;
const response = await this.cfExecute(["curl", uri]);
return this.parseJson(response)?.resources || [];
})).then(resources => [].concat(...resources)));
}
return resources ?? [];
}
static getOAuthToken() {
return this.cfExecute(["oauth-token"]);
}
static async cfExecute(params) {
const MAX_ATTEMPTS = 3;
const errors = new Set();
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
try {
const response = await Cli.execute(params, { env: { "CF_COLOR": "false" } });
if (response.exitCode === 0) {
const errorValues = [...errors.values()];
if (errorValues?.length > 0) {
log.verbose(this.errorsToString(errorValues));
}
return response.stdout;
}
errors.add(response.error || response.stderr);
}
catch (error) {
errors.add(error.message);
}
}
throw new Error(`Failed to send request with parameters '${JSON.stringify(params)}': ${this.errorsToString([...errors.values()])}`);
}
static errorsToString(errors) {
return errors.length > 1
? errors.map((error, attempt) => `${attempt + 1} attempt: ${error}`).join("; ")
: errors.map(error => error);
}
static parseJson(jsonString) {
try {
return JSON.parse(jsonString);
}
catch (error) {
throw new Error(`Failed parse response from request CF API: ${error.message}`);
}
}
/**
* Get space guid from configuration or local CF fodler
* @static
* @param {string} spaceGuid ui5.yaml options
* @return {Promise<string>} promise with space guid
* @memberof CFUtil
*/
static async getSpaceGuid(spaceGuid) {
return spaceGuid ?? getSpaceGuidThrowIfUndefined().catch((e) => {
throw new Error("Please specify space and org guids in ui5.yaml or login to Cloud Foundry with 'cf login' and try again: " + e.message);
});
}
}
//# sourceMappingURL=cfUtil.js.map