UNPKG

@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
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