UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

174 lines (173 loc) 9.48 kB
import cliProgress from "cli-progress"; import path from "path"; import { getPresignedURL } from "../../requests/getPresignedURL.js"; import { debugLogger } from "../../utils/logging.js"; import { uploadContentToS3 } from "../../requests/uploadContentToS3.js"; import { log } from "../../utils/logging.js"; import { deployRequest } from "../../requests/deployCode.js"; import { GenezioCloudInputType, } from "../cloudAdapter.js"; import { createTemporaryFolder, deleteFolder, zipDirectoryToDestinationPath, } from "../../utils/file.js"; import { createFrontendProject } from "../../requests/createFrontendProject.js"; import { getFrontendPresignedURL } from "../../requests/getFrontendPresignedURL.js"; import { getFileSize } from "../../utils/file.js"; import { CloudProviderIdentifier } from "../../models/cloudProviderIdentifier.js"; import { calculateBiggestFiles } from "../../utils/calculateBiggestProjectFiles.js"; import Table from "cli-table"; import { UserError } from "../../errors.js"; import { stdout } from "process"; import { createHash } from "../../utils/strings.js"; // The maximum size of a bundle is 1.524e9 bytes approx 1.5GB. const BUNDLE_SIZE_LIMIT = 1.524e9; const PERSISTENT_BUNDLE_SIZE_LIMIT = 5.0e9; // 5GB async function handleBigElementSizeError(element, projectConfiguration, BUNDLE_SIZE_LIMIT = 1.524e9) { const size = await getFileSize(element.archivePath); if (size > BUNDLE_SIZE_LIMIT) { throw new UserError(`Your ${element.type} ${element.name} is too big: ${size} bytes. The maximum size is ${(BUNDLE_SIZE_LIMIT / 1048576).toFixed(2)}MB. Try to reduce the size of your ${element.type}.`); } if (element.unzippedBundleSize > BUNDLE_SIZE_LIMIT && element.type === GenezioCloudInputType.FUNCTION) { throw new UserError(`Your function ${element.name} is too big: ${element.unzippedBundleSize} bytes. The maximum size is ${(BUNDLE_SIZE_LIMIT / 1048576).toFixed(2)}MB. Try to reduce the size of your function.`); } if (!(element.unzippedBundleSize > BUNDLE_SIZE_LIMIT && element.type === GenezioCloudInputType.CLASS)) { debugLogger.debug(`The bundle for ${element.type} ${element.name} is ${(size / 1048576).toFixed(2)}MB out of ${(BUNDLE_SIZE_LIMIT / 1048576).toFixed(2)}MB.`); return; } const { dependenciesInfo, allNonJsFilesPaths } = element; // Throw this error if bundle size is too big and the user is not using js or ts files. if (!dependenciesInfo || !allNonJsFilesPaths) { throw new UserError(`Your ${element.type} ${element.name} is too big: ${element.unzippedBundleSize} bytes. The maximum size is ${(BUNDLE_SIZE_LIMIT / 1048576).toFixed(2)}MB. Try to reduce the size of your class.`); } const allfilesSize = await calculateBiggestFiles(dependenciesInfo, allNonJsFilesPaths); const dependenciesTable = new Table({ head: ["Biggest Dependencies", "Size"], }); const filesTable = new Table({ head: [ `Biggest Non-${projectConfiguration.classes[0].language.toUpperCase()} Files`, "Size", ], }); const maxLength = Math.max(allfilesSize.dependenciesSize.length, allfilesSize.filesSize.length); for (let i = 0; i < maxLength; i++) { const formatedDep = allfilesSize.dependenciesSize[i] ? allfilesSize.dependenciesSize[i] : ""; const formatedNonJsFile = allfilesSize.filesSize[i] ? allfilesSize.filesSize[i] : ""; if (formatedDep.split("->")[0] && formatedDep.split("->")[1]) { dependenciesTable.push([formatedDep.split("->")[0], formatedDep.split("->")[1]]); } if (formatedNonJsFile.split("->")[0] && formatedNonJsFile.split("->")[1]) { filesTable.push([formatedNonJsFile.split("->")[0], formatedNonJsFile.split("->")[1]]); } } log.info(dependenciesTable.toString()); log.info(filesTable.toString()); throw new UserError(` Class ${element.name} is too big: ${(element.unzippedBundleSize / 1048576).toFixed(2)}MB. The maximum size is ${BUNDLE_SIZE_LIMIT / 1048576}MB. Try to reduce the size of your class. `); } export class GenezioCloudAdapter { async deploy(input, projectConfiguration, cloudAdapterOptions, stack = [], sourceRepository, environmentVariables) { const stage = cloudAdapterOptions.stage || ""; log.info("Deploying your backend project to the genezio infrastructure..."); const multibar = new cliProgress.MultiBar({ stream: stdout, clearOnComplete: false, hideCursor: true, format: "Uploading {filename}: {bar} | {value}% | {eta_formatted}", }, cliProgress.Presets.shades_grey); const promisesDeploy = input.map(async (element) => { await handleBigElementSizeError(element, projectConfiguration, element.persistent ? PERSISTENT_BUNDLE_SIZE_LIMIT : BUNDLE_SIZE_LIMIT); debugLogger.debug(`Get the presigned URL for ${element.type}: ${element.name} ${element.archiveName}.`); const presignedUrl = await getPresignedURL(projectConfiguration.region, element.archiveName ?? "genezioDeploy.zip", projectConfiguration.name, element.name); const bar = multibar.create(100, 0, { filename: element.name }); debugLogger.debug(`Upload the content to S3 for ${element.type}: ${element.name}..`); await uploadContentToS3(presignedUrl, element.archivePath, (percentage) => { bar.update(parseFloat((percentage * 100).toFixed(2)), { filename: element.name, }); if (percentage == 1) { bar.stop(); } }); debugLogger.debug(`Done uploading the content to S3 for ${element.type}: ${element.name}..`); }); // wait for all promises to finish await Promise.all(promisesDeploy); multibar.stop(); // The loading spinner is removing lines and with this we avoid clearing a progress bar. // This can be removed only if we find a way to avoid clearing lines. log.info(""); const response = await deployRequest(projectConfiguration, input, stage, stack, sourceRepository, environmentVariables); const classesInfo = response.classes.map((c) => ({ className: c.name, methods: c.methods.map((m) => ({ name: m.name, type: m.type, cronString: m.cronString, functionUrl: getFunctionUrl(c.cloudUrl, m.type, c.name, m.name), })), functionUrl: getClassFunctionUrl(c.cloudUrl, projectConfiguration.cloudProvider, c.name), projectId: response.projectId, })); return { projectId: response.projectId, projectEnvId: response.projectEnvId, classes: classesInfo, functions: response.functions, }; } async deployFrontend(projectName, projectRegion, frontend, stage) { const finalStageName = stage != "" && stage != "prod" ? `-${stage.replaceAll(/[/_.]/gm, "-")}` : ""; let finalSubdomain = frontend.subdomain + finalStageName; if (finalSubdomain.length > 63) { debugLogger.debug("Subdomain is too long. Generating random subdomain."); finalSubdomain = frontend.subdomain?.substring(0, 55) + "-" + createHash(stage, 4); } const archivePath = path.join(await createTemporaryFolder(), `${finalSubdomain}.zip`); debugLogger.debug("Creating temporary folder", archivePath); const frontendPath = path.join(frontend.path, frontend.publish || "."); await zipDirectoryToDestinationPath(frontendPath, finalSubdomain, archivePath, /* includeHiddenFiles= */ true, /* exclusions= */ [".git", ".github"]); debugLogger.debug("Getting presigned URL..."); const result = await getFrontendPresignedURL(finalSubdomain, projectName, stage); if (!result.presignedURL) { throw new UserError("An error occurred (missing presignedUrl). Please try again!"); } if (!result.userId) { throw new UserError("An error occurred (missing userId). Please try again!"); } debugLogger.debug("Content of the folder zipped. Uploading to S3."); await uploadContentToS3(result.presignedURL, archivePath, undefined, result.userId); debugLogger.debug("Uploaded to S3."); const finalDomain = await createFrontendProject(finalSubdomain, projectName, projectRegion, stage, [...(frontend.redirects || []), ...(frontend.rewrites || [])]); // clean up temporary folder await deleteFolder(path.dirname(archivePath)); return finalDomain; } } function getFunctionUrl(baseUrl, methodType, className, methodName) { if (methodType === "http") { // trim the last slash of baseUrl if it exists if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, -1); } return `${baseUrl}/${className}/${methodName}`; } else { return `${baseUrl}/${className}`; } } function getClassFunctionUrl(baseUrl, cloudProvider, className) { if (cloudProvider === CloudProviderIdentifier.GENEZIO_UNIKERNEL || cloudProvider === CloudProviderIdentifier.GENEZIO_CLOUD) { return baseUrl; } else { return `${baseUrl}${className}`; } }