UNPKG

genezio

Version:

Command line utility to interact with Genezio infrastructure.

884 lines (883 loc) 40.1 kB
import path from "path"; import git from "isomorphic-git"; import fs from "fs"; import { ADD_DATABASE_CONFIG, UserError } from "../../errors.js"; import { CloudProviderIdentifier } from "../../models/cloudProviderIdentifier.js"; import dns from "dns"; import { promisify } from "util"; import { AuthenticationDatabaseType, AuthenticationEmailTemplateType, DatabaseType, } from "../../projectConfiguration/yaml/models.js"; import { YamlConfigurationIOController } from "../../projectConfiguration/yaml/v2.js"; import { createDatabase, findLinkedDatabase, getDatabaseByName, linkDatabaseToEnvironment, } from "../../requests/database.js"; import { DASHBOARD_URL } from "../../constants.js"; import getProjectInfoByName, { getProjectEnvFromProjectByName, getProjectInfoByNameIfExits, } from "../../requests/getProjectInfoByName.js"; import { createEmptyProject } from "../../requests/project.js"; import { debugLogger } from "../../utils/logging.js"; import { createTemporaryFolder, fileExists, readEnvironmentVariablesFile, zipDirectory, } from "../../utils/file.js"; import { resolveConfigurationVariable } from "../../utils/scripts.js"; import { log } from "../../utils/logging.js"; import colors from "colors"; import { findAnEnvFile, getUnsetEnvironmentVariables, parseConfigurationVariable, promptToConfirmSettingEnvironmentVariables, } from "../../utils/environmentVariables.js"; import inquirer from "inquirer"; import { existsSync, readFileSync, readdirSync } from "fs"; import { checkProjectName } from "../create/create.js"; import { uniqueNamesGenerator, adjectives, animals } from "unique-names-generator"; import { regions } from "../../utils/configs.js"; import { isCI } from "../../utils/process.js"; import { getAuthentication, getAuthProviders, setAuthentication, setAuthProviders, setEmailTemplates, } from "../../requests/authentication.js"; import { getPresignedURLForProjectCodePush } from "../../requests/getPresignedURLForProjectCodePush.js"; import { uploadContentToS3 } from "../../requests/uploadContentToS3.js"; import { displayHint, replaceExpression } from "../../utils/strings.js"; import { packageManagers } from "../../packageManagers/packageManager.js"; import gitignore from "parse-gitignore"; import { disableEmailIntegration, enableEmailIntegration, getProjectIntegrations, } from "../../requests/integration.js"; import { ContainerComponentType, SSRFrameworkComponentType } from "../../models/projectOptions.js"; const dnsLookup = promisify(dns.lookup); export var EnvironmentResourceType; (function (EnvironmentResourceType) { EnvironmentResourceType["RemoteResourceReference"] = "remoteResourceReference"; EnvironmentResourceType["EnvironmentFileReference"] = "environmentFileReference"; EnvironmentResourceType["LiteralValue"] = "literalValue"; })(EnvironmentResourceType || (EnvironmentResourceType = {})); export async function prepareServicesPreBackendDeployment(configuration, projectName, environment, envFile) { if (!configuration.services) { debugLogger.debug("No services found in the configuration file."); return; } const projectDetails = await getOrCreateEmptyProject(projectName, configuration.region, environment || "prod"); if (!projectDetails) { throw new UserError("Could not create project."); } if (configuration.services?.databases) { const databases = configuration.services.databases; for (const database of databases) { if (!database.region) { database.region = configuration.region; } let createdDatabaseRequest = { name: database.name, region: database.region, type: database.type, }; if (database.type === DatabaseType.mongo) { createdDatabaseRequest = { ...createdDatabaseRequest, clusterType: database.clusterType, clusterName: database.clusterName, clusterTier: database.clusterTier, }; } await getOrCreateDatabase(createdDatabaseRequest, environment || "prod", projectDetails.projectId, projectDetails.projectEnvId); } } if (configuration.services?.email !== undefined) { const isEnabled = (await getProjectIntegrations(projectDetails.projectId, projectDetails.projectEnvId)).integrations.find((integration) => integration === "EMAIL-SERVICE"); if (configuration.services?.email && !isEnabled) { await enableEmailIntegration(projectDetails.projectId, projectDetails.projectEnvId); log.info("Email integration enabled successfully."); } else if (configuration.services?.email === false && isEnabled) { await disableEmailIntegration(projectDetails.projectId, projectDetails.projectEnvId); log.info("Email integration disabled successfully."); } } if (configuration.services?.authentication) { await enableAuthentication(configuration, projectDetails.projectId, projectDetails.projectEnvId, environment || "prod", envFile || (await findAnEnvFile(process.cwd()))); } } export async function prepareServicesPostBackendDeployment(configuration, projectName, optionsStage) { if (!configuration.services) { debugLogger.debug("No services found in the configuration file."); return; } const settings = configuration.services?.authentication?.settings; if (settings) { const stage = optionsStage || "prod"; const projectEnv = await getProjectEnvFromProjectByName(projectName, stage); if (!projectEnv) { throw new UserError(`Stage ${stage} not found in project ${projectName}. Please run 'genezio deploy --stage ${stage}' to deploy your project to a new stage.`); } if (settings?.resetPassword) { await setAuthenticationEmailTemplates(configuration, settings.resetPassword.redirectUrl, AuthenticationEmailTemplateType.passwordReset, stage, projectEnv?.id); } if (settings.emailVerification) { await setAuthenticationEmailTemplates(configuration, settings.emailVerification.redirectUrl, AuthenticationEmailTemplateType.verification, stage, projectEnv?.id); } } } export async function getOrCreateEmptyProject(projectName, region, stage = "prod", ask = false) { const project = await getProjectInfoByName(projectName).catch((error) => { if (error instanceof UserError && error.message.includes("record not found")) { return undefined; } debugLogger.debug(`Error getting project ${projectName}: ${error}`); throw new UserError(`Failed to get project ${projectName}: ${error}`); }); const projectEnv = project?.projectEnvs.find((projectEnv) => projectEnv.name == stage); if (!project || !projectEnv) { if (ask) { const { createProject } = await inquirer.prompt([ { type: "confirm", name: "createProject", message: `Project ${projectName} not found remotely. Do you want to create it?`, default: false, }, ]); if (!createProject) { log.warn(`Project ${projectName} not found and you chose not to create it.`); return undefined; } log.info(`Creating project ${projectName} in region ${region} on stage ${stage}...`); } const newProject = await createEmptyProject({ projectName: projectName, region: region, cloudProvider: CloudProviderIdentifier.GENEZIO_CLOUD, stage: stage, }).catch((error) => { debugLogger.debug(`Error creating project ${projectName}: ${error}`); throw new UserError(`Failed to create project ${projectName}.`); }); log.info(colors.green(`Project ${projectName} in region ${region} on stage ${stage} was created successfully`)); return { projectId: newProject.projectId, projectEnvId: newProject.projectEnvId }; } return { projectId: project.id, projectEnvId: projectEnv.id }; } export async function attemptToInstallDependencies(args = [], currentPath, packageManagerType, cleanInstall = false) { const packageManager = packageManagers[packageManagerType]; debugLogger.debug(`Attempting to install dependencies with ${packageManager.command} ${args.join(" ")}`); try { if (!cleanInstall) { await packageManager.install(args, currentPath); } else { await packageManager.cleanInstall(currentPath, args); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("code EJSONPARSE")) { throw new UserError(`Failed to install dependencies due to an invalid package.json file. Please fix your package.json file.`); } if (errorMessage.includes("code E404")) { const extractPackageName = getMissingPackage(errorMessage); throw new UserError(`Failed to install dependencies due to a missing package: ${extractPackageName}. Please fix your package.json file`); } if (errorMessage.includes("code ETARGET")) { const noTargetPackage = getNoTargetPackage(errorMessage); throw new UserError(`Failed to install dependencies due to a non-existent package version: ${noTargetPackage}. Please fix your package.json file`); } if (errorMessage.includes("code ERESOLVE") && !args.includes("--legacy-peer-deps")) { return attemptToInstallDependencies([...args, "--legacy-peer-deps"], currentPath, packageManagerType); } throw new UserError(`Failed to install dependencies: ${errorMessage}`); } const command = `${packageManager.command} install${args.length ? ` ${args.join(" ")}` : ""}`; log.info(`Dependencies installed successfully with command: ${command}`); return { command: command, args: args, }; } function getMissingPackage(errorMessage) { const missingPackageRegex = /'([^@]+@[^']+)' is not in this registry/; const match = errorMessage.match(missingPackageRegex); return match ? match[1] : null; } function getNoTargetPackage(errorMessage) { const noTargetPackageRegex = /No matching version found for ([^@]+@[^.]+)/; const match = errorMessage.match(noTargetPackageRegex); return match ? match[1] : null; } export async function hasInternetConnection() { const testDomain = "google.com"; const timeout = 5000; try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("DNS lookup timed out")); }, timeout); }); await Promise.race([dnsLookup(testDomain), timeoutPromise]); return true; } catch (error) { debugLogger.debug(`Error checking internet connection: ${error}`); return false; } } export async function readOrAskConfig(configPath, givenName, givenRegion) { const configIOController = new YamlConfigurationIOController(configPath); if (!existsSync(configPath)) { const name = givenName || (await readOrAskProjectName()); let region = givenRegion || regions[0].value; if (!isCI() && !givenRegion) { ({ region } = await inquirer.prompt([ { type: "list", name: "region", message: "Select the Genezio project region:", choices: regions, }, ])); } await configIOController.write({ name, region, yamlVersion: 2 }); } return await configIOController.read(); } export async function readOrAskProjectName() { const repositoryUrl = (await git.listRemotes({ fs, dir: process.cwd() })).find((r) => r.remote === "origin")?.url; let basename; if (repositoryUrl) { const repositoryName = path.basename(repositoryUrl, ".git"); basename = repositoryName; const validProjectName = await (async () => checkProjectName(repositoryName))() .then(() => true) .catch(() => false); const projectExists = await getProjectInfoByName(repositoryName) .then(() => true) .catch(() => false); // We don't want to automatically use the repository name if the project // exists, because it could overwrite the existing project by accident. if (repositoryName !== undefined && validProjectName && !projectExists) return repositoryName; } if (existsSync("package.json")) { // Read package.json content const packageJson = readFileSync("package.json", "utf-8"); const packageJsonName = JSON.parse(packageJson)["name"]; const validProjectName = await (async () => checkProjectName(packageJsonName))() .then(() => true) .catch(() => false); const projectExists = await getProjectInfoByName(packageJsonName) .then(() => true) .catch(() => false); // We don't want to automatically use the package.json name if the project // exists, because it could overwrite the existing project by accident. if (packageJsonName !== undefined && validProjectName && !projectExists) return packageJsonName; } let name = basename ? `${basename}-${Math.random().toString(36).substring(2, 7)}` : uniqueNamesGenerator({ dictionaries: [adjectives, animals], separator: "-", style: "lowerCase", length: 2, }); if (!isCI()) { // Ask for project name ({ name } = await inquirer.prompt([ { type: "input", name: "name", message: "Enter the Genezio project name:", default: path.basename(process.cwd()), validate: (input) => { try { checkProjectName(input); return true; } catch (error) { if (error instanceof Error) return colors.red(error.message); return colors.red("Unavailable project name"); } }, }, ])); } else { debugLogger.debug("Using a random name for the project because no `genezio.yaml` file was found."); } return name; } /** * To prevent reusing an already deployed database, we will check if the database exists remotely. * @param prefix Prefix for the database name, usually the engine used - e.g. "postgres", "mongo" * @returns A unique database name */ export async function generateDatabaseName(prefix) { const defaultDatabaseName = "my-" + prefix + "-db"; const databaseExists = await getDatabaseByName(defaultDatabaseName) .then((response) => { return response !== undefined; }) .catch(() => false); if (!databaseExists) { return defaultDatabaseName; } debugLogger.debug(`Database ${defaultDatabaseName} already exists. Generating a new database name...`); const generatedDatabaseName = uniqueNamesGenerator({ dictionaries: [adjectives, animals], separator: "-", style: "lowerCase", length: 2, }) + "-db"; return generatedDatabaseName; } export async function getOrCreateDatabase(createDatabaseReq, stage, projectId, projectEnvId, ask = false) { const linkedDatabase = await findLinkedDatabase(createDatabaseReq.name, projectId, projectEnvId).catch((error) => { debugLogger.debug(`Error finding linked database ${createDatabaseReq.name}: ${error}`); throw new UserError(`Failed to find the linked database ${createDatabaseReq.name}.`); }); if (linkedDatabase) { debugLogger.debug(`Database ${createDatabaseReq.name} is already linked to stage ${stage}`); return linkedDatabase; } log.info(`Database ${createDatabaseReq.name} is not linked. Proceeding to link it...`); const database = await getDatabaseByName(createDatabaseReq.name); if (database) { debugLogger.debug(`Database ${createDatabaseReq.name} is already created.`); if (database.region.replace("aws-", "") !== createDatabaseReq.region) { log.warn(`Database ${createDatabaseReq.name} is created in a different region ${database.region}.`); log.warn(`To change the region, you need to delete the database and create a new one at ${colors.cyan(`${DASHBOARD_URL}/databases`)}`); } if (ask) { const { linkDatabase } = await inquirer.prompt([ { type: "confirm", name: "linkDatabase", message: `Database ${createDatabaseReq.name} is not linked. Do you want to link it to stage ${stage}?`, default: false, }, ]); if (!linkDatabase) { log.warn(`Database ${createDatabaseReq.name} is not linked and you chose not to link it.`); return undefined; } } await linkDatabaseToEnvironment(projectId, projectEnvId, database.id).catch((error) => { debugLogger.debug(`Error linking database ${createDatabaseReq.name}: ${error}`); throw new UserError(`Failed to link database ${createDatabaseReq.name}.`); }); log.info(colors.green(`Database ${createDatabaseReq.name} was linked successfully to stage ${stage}`)); return database; } if (ask) { const { createDatabase } = await inquirer.prompt([ { type: "confirm", name: "createDatabase", message: `Database ${createDatabaseReq.name} not found remotely. Do you want to create it?`, default: false, }, ]); if (!createDatabase) { log.warn(`Database ${createDatabaseReq.name} not found and you chose not to create it.`); return undefined; } log.info(`Creating database ${createDatabaseReq.name} in region ${createDatabaseReq.region}...`); } const newDatabase = await createDatabase(createDatabaseReq, projectId, projectEnvId, true).catch((error) => { debugLogger.debug(`Error creating database ${createDatabaseReq.name}: ${error}`); throw new UserError(`Failed to create database ${createDatabaseReq.name}.`); }); log.info(colors.green(`Database ${createDatabaseReq.name} created successfully.`)); log.info(displayHint(`You can reference the connection URI in your \`genezio.yaml\` file using \${{services.databases.${createDatabaseReq.name}.uri}}`)); return { id: newDatabase.databaseId, name: createDatabaseReq.name, region: createDatabaseReq.region, type: createDatabaseReq.type || DatabaseType.neon, }; } function isYourOwnAuthDatabaseConfig(object) { return typeof object === "object" && object !== null && "uri" in object && "type" in object; } export async function enableAuthentication(configuration, projectId, projectEnvId, stage, envFile, ask = false) { const authDatabase = configuration.services?.authentication?.database; if (!authDatabase) { return; } // If authentication.providers is not set, all auth providers are disabled by default const authProviders = configuration.services?.authentication?.providers ? configuration.services?.authentication?.providers : { email: false, google: undefined, web3: false, }; const authenticationStatus = await getAuthentication(projectEnvId); if (authenticationStatus.enabled) { const remoteAuthProviders = await getAuthProviders(projectEnvId); if (!haveAuthProvidersChanged(remoteAuthProviders.authProviders, authProviders)) { log.info("Authentication is already enabled."); log.info("The corresponding auth providers are already set."); return; } } if (authProviders.google) { const clientId = await evaluateResource(configuration, [ EnvironmentResourceType.EnvironmentFileReference, EnvironmentResourceType.RemoteResourceReference, EnvironmentResourceType.LiteralValue, ], authProviders.google?.clientId, stage, envFile); const clientSecret = await evaluateResource(configuration, [ EnvironmentResourceType.EnvironmentFileReference, EnvironmentResourceType.RemoteResourceReference, EnvironmentResourceType.LiteralValue, ], authProviders.google.clientSecret, stage, envFile); if (!clientId || !clientSecret) { throw new UserError("Google authentication is enabled but the client ID or client secret is missing."); } authProviders.google = { clientId, clientSecret, }; } if (isYourOwnAuthDatabaseConfig(authDatabase)) { const databaseUri = await evaluateResource(configuration, [ EnvironmentResourceType.EnvironmentFileReference, EnvironmentResourceType.RemoteResourceReference, EnvironmentResourceType.LiteralValue, ], authDatabase.uri, stage, envFile); await enableAuthenticationHelper({ enabled: true, databaseUrl: databaseUri, databaseType: authDatabase.type, }, projectEnvId, authProviders, /* ask= */ ask); } else { const configDatabase = configuration.services?.databases?.find((database) => database.name === authDatabase.name); if (!configDatabase) { throw new UserError(ADD_DATABASE_CONFIG(authDatabase.name, configuration.region)); } if (!configDatabase.region) { configDatabase.region = configuration.region; } let createdDatabaseRequest = { name: configDatabase.name, region: configDatabase.region, type: configDatabase.type, }; if (configDatabase.type === DatabaseType.mongo) { createdDatabaseRequest = { ...createdDatabaseRequest, clusterType: configDatabase.clusterType, clusterName: configDatabase.clusterName, clusterTier: configDatabase.clusterTier, }; } const database = await getOrCreateDatabase(createdDatabaseRequest, stage, projectId, projectEnvId); if (!database) { return; } let databaseType; switch (database.type) { case DatabaseType.neon: databaseType = AuthenticationDatabaseType.postgres; break; case DatabaseType.mongo: databaseType = AuthenticationDatabaseType.mongo; break; default: throw new UserError(`Database type ${database.type} is not supported.`); } await enableAuthenticationHelper({ enabled: true, databaseUrl: database.connectionUrl || "", databaseType, }, projectEnvId, authProviders, /* ask= */ ask); } } export async function enableAuthenticationHelper(request, projectEnvId, providers, ask = false) { if (ask) { const { enableAuthentication } = await inquirer.prompt([ { type: "confirm", name: "enableAuthentication", message: "Authentication is not enabled or providers are not updated. Do you want to update this service?", default: false, }, ]); if (!enableAuthentication) { log.warn("Authentication is not enabled."); return; } log.info(`Enabling authentication...`); } await setAuthentication(projectEnvId, request); const authProvidersResponse = await getAuthProviders(projectEnvId); const providersDetails = []; if (providers) { for (const provider of authProvidersResponse.authProviders) { let enabled = false; switch (provider.name) { case "email": { if (providers.email) { enabled = true; } providersDetails.push({ id: provider.id, name: provider.name, enabled: enabled, config: null, }); break; } case "web3": { if (providers.web3) { enabled = true; } providersDetails.push({ id: provider.id, name: provider.name, enabled: enabled, config: null, }); break; } case "google": { if (providers.google) { enabled = true; } providersDetails.push({ id: provider.id, name: provider.name, enabled: enabled, config: { GNZ_AUTH_GOOGLE_ID: providers.google?.clientId || "", GNZ_AUTH_GOOGLE_SECRET: providers.google?.clientSecret || "", }, }); break; } } } // If providers details are updated, call the setAuthProviders method if (providersDetails.length > 0) { const setAuthProvidersRequest = { authProviders: providersDetails, }; await setAuthProviders(projectEnvId, setAuthProvidersRequest); debugLogger.debug(`Authentication providers: ${JSON.stringify(providersDetails)} set successfully.`); } } log.info(colors.green(`Authentication enabled successfully.`)); return; } function haveAuthProvidersChanged(remoteAuthProviders, authProviders) { for (const provider of remoteAuthProviders) { switch (provider.name) { case "email": if (!!authProviders.email !== provider.enabled) { return true; } break; case "web3": if (!!authProviders.web3 !== provider.enabled) { return true; } break; case "google": if (!!authProviders.google !== provider.enabled) { return true; } break; } } return false; } export async function setAuthenticationEmailTemplates(configuration, redirectUrlRaw, type, stage, projectEnvId) { const redirectUrl = await evaluateResource(configuration, [ EnvironmentResourceType.EnvironmentFileReference, EnvironmentResourceType.RemoteResourceReference, EnvironmentResourceType.LiteralValue, ], redirectUrlRaw, stage, undefined); await setEmailTemplates(projectEnvId, { templates: [ { type: type, template: { redirectUrl: redirectUrl, }, }, ], }); type === AuthenticationEmailTemplateType.verification ? log.info(colors.green(`Email verification field set successfully.`)) : log.info(colors.green(`Password reset field set successfully.`)); } /** * Evaluates a resource by resolving its value based on the allowed resource types. * * @param configuration - The project configuration used for resolving remote references. * @param allowedResourceTypes - Specifies which types of resource references are permitted: * * - `EnvironmentResourceType.RemoteResourceReference`: A reference to a structured configuration variable, such as a URL for a backend function. * - Example: `"${{ backend.functions.<function-name>.url }}"` * - Output: Resolves to the function's URL from the project configuration. * * - `EnvironmentResourceType.EnvironmentFileReference`: A reference to an environment variable stored in a `.env` file or `process.env`. * - Requires `envFile`. * - Example: `"${{ env.MY_ENV_VAR }}"` * - Output: Resolves the value from the specified `.env` file. * * - `EnvironmentResourceType.LiteralValue`: A plain, raw value that does not require resolution. * - No additional parameters required. * - Example: `"my-value"` * - Output: `"my-value"` * */ export async function evaluateResource(configuration, allowedResourceTypes, resource, stage, envFile, options) { if (!resource) { return ""; } const resourceRaw = await parseConfigurationVariable(resource); if (allowedResourceTypes?.includes(EnvironmentResourceType.RemoteResourceReference) && "path" in resourceRaw && "field" in resourceRaw) { const resourceValue = await resolveConfigurationVariable(configuration, stage || "prod", resourceRaw.path, resourceRaw.field, options); return replaceExpression(resource, resourceValue); } if (allowedResourceTypes?.includes(EnvironmentResourceType.EnvironmentFileReference) && "key" in resourceRaw) { if (options?.isFrontend) { log.warn(`Environment variable placeholders like $\{{env.ENV_VAR}} are not supported in \`frontend.environment\`. Please use the literal value or set it in a \`.env\` file.`); return ""; } // search for the environment variable in process.env const resourceFromProcessValue = process.env[resourceRaw.key]; if (resourceFromProcessValue) { return replaceExpression(resource, resourceFromProcessValue); } if (!envFile) { throw new UserError(`Environment variable file was not provided to set $\{{ env.${resourceRaw.key} }}. Please provide the correct path with \`genezio deploy --env <envFile>\` or \`genezio local --env <envFile>\`.`); } if (!(await fileExists(envFile))) { throw new UserError(`Environment variable file ${envFile} was not found. Please provide the correct path with \`genezio deploy --env <envFile>\` or \`genezio local --env <envFile>\`.`); } const resourceValue = (await readEnvironmentVariablesFile(envFile)).find((envVar) => envVar.name === resourceRaw.key)?.value; if (!resourceValue) { throw new UserError(`Environment variable ${resourceRaw.key} is missing from the ${envFile} file.`); } return replaceExpression(resource, resourceValue); } if (allowedResourceTypes.includes(EnvironmentResourceType.LiteralValue)) { return resourceRaw.value; } return ""; } export async function actionDetectedEnvFile(cwd, projectName, stage) { const envFile = ".env"; const envFileFullPath = path.join(cwd, envFile); if (!(await fileExists(envFileFullPath))) { return undefined; } const envVars = await readEnvironmentVariablesFile(envFileFullPath); if (envVars.length === 0) { return undefined; } let missingEnvVarsKeys = envVars.map((envVar) => envVar.name); const project = await getProjectInfoByNameIfExits(projectName); if (project) { const projectEnv = project.projectEnvs.find((projectEnv) => projectEnv.name == stage); if (projectEnv) { missingEnvVarsKeys = await getUnsetEnvironmentVariables(envVars.map((envVar) => envVar.name), project.id, projectEnv.id); } } if (missingEnvVarsKeys.length === 0) { return undefined; } const confirmSettingEnvVars = await promptToConfirmSettingEnvironmentVariables(missingEnvVarsKeys); if (!confirmSettingEnvVars) { log.info(`Skipping environment variables upload. You can set them later by navigating to the dashboard.`); return undefined; } const envRelativePath = path.relative(process.cwd(), envFileFullPath); debugLogger.debug(`Detected environment variables file at ${envRelativePath}. It will be considered for deployment.`); return envRelativePath; } export async function createBackendEnvVarList(optionsEnvFileFlag, stage, configuration, componentType = "backend") { const envVars = []; // Get literal values from [backend|nextjs|nuxt|remix|streamlit].environment const environment = getEnvironmentConfiguration(configuration, componentType); if (environment) { const environmentVariablesLiterals = await evaluateEnvironmentVariablesFromConfiguration(environment, configuration, stage, [EnvironmentResourceType.LiteralValue]); debugLogger.debug(`Environment variables set from literal values: ${JSON.stringify(environmentVariablesLiterals)}`); envVars.push(...environmentVariablesLiterals); } // Get ${{ env.<KEY> }} variables from [backend|nextjs|nuxt|remix|streamlit].environmentgit if (environment) { const environmentVariablesFromConfigFile = await evaluateEnvironmentVariablesFromConfiguration(environment, configuration, stage, [EnvironmentResourceType.EnvironmentFileReference], optionsEnvFileFlag); envVars.push(...environmentVariablesFromConfigFile); } if (!optionsEnvFileFlag) { return envVars; } const envFile = path.join(process.cwd(), optionsEnvFileFlag); if (!(await fileExists(envFile))) { throw new UserError(`File ${envFile} does not exist. Please provide the correct path using \`--env <envFile>\`.`); } // Get environment variables from the .env file envVars.push(...(await readEnvironmentVariablesFile(envFile)).filter((envVar) => !envVars.find((v) => v.name === envVar.name))); debugLogger.debug(`The following environment variables will be set during deployment: ${envVars.map((envVar) => envVar.name).join(", ")}`); return envVars; } async function evaluateEnvironmentVariablesFromConfiguration(environment, configuration, stage, allowedResourceTypes, envFile) { const envVarKeys = Object.keys(environment); return (await Promise.all(envVarKeys.map(async (envVarKey) => { const value = await evaluateResource(configuration, allowedResourceTypes, environment[envVarKey], stage, envFile); return value ? { name: envVarKey, value } : undefined; }))).filter(Boolean); } export async function createBackendEnvVarListFromRemote(environment, configuration, stage) { const envVarKeys = Object.keys(environment); return (await Promise.all(envVarKeys.map(async (envVarKey) => { const value = await evaluateResource(configuration, [EnvironmentResourceType.RemoteResourceReference], environment[envVarKey], stage, /* envFile= */ undefined); return value ? { name: envVarKey, value } : undefined; }))).filter(Boolean); } function getEnvironmentConfiguration(configuration, componentType) { return ({ [ContainerComponentType.container]: configuration.container?.environment, [SSRFrameworkComponentType.next]: configuration.nextjs?.environment, [SSRFrameworkComponentType.nuxt]: configuration.nuxt?.environment, [SSRFrameworkComponentType.nitro]: configuration.nitro?.environment, [SSRFrameworkComponentType.nestjs]: configuration.nestjs?.environment, [SSRFrameworkComponentType.remix]: configuration.remix?.environment, [SSRFrameworkComponentType.streamlit]: configuration.streamlit?.environment, backend: configuration.backend?.environment, }[componentType] ?? configuration.backend?.environment); } // Variables to be excluded from the zip file for the project code (.genezioignore) export const excludedFiles = [ "projectCode.zip", "**/projectCode.zip", "**/node_modules/*", "./node_modules/*", "node_modules/*", "**/node_modules", "./node_modules", "node_modules", "node_modules/**", "**/node_modules/**", // ignore all .git files "**/.git/*", "./.git/*", ".git/*", "**/.git", "./.git", ".git", ".git/**", "**/.git/**", // ignore all .next files "**/.next/*", "./.next/*", ".next/*", "**/.next", "./.next", ".next", ".next/**", "**/.next/**", // ignore all .open-next files "**/.open-next/*", "./.open-next/*", ".open-next/*", "**/.open-next", "./.open-next", ".open-next", ".open-next/**", "**/.open-next/**", // ignore all .vercel files "**/.vercel/*", "./.vercel/*", ".vercel/*", "**/.vercel", "./.vercel", ".vercel", ".vercel/**", "**/.vercel/**", // ignore all .turbo files "**/.turbo/*", "./.turbo/*", ".turbo/*", "**/.turbo", "./.turbo", ".turbo", ".turbo/**", "**/.turbo/**", // ignore all .sst files "**/.sst/*", "./.sst/*", ".sst/*", "**/.sst", "./.sst", ".sst", ".sst/**", "**/.sst/**", // ignore env files ".env", ".env.development.local", ".env.test.local", ".env.production.local", ".env.local", // ignore python virtual environment "**/venv/*", ".venv/*", "venv/*", "**/venv", ".venv", "venv", // python cache "**/__pycache__/*", ".pycache/*", ".pycache", "**/__pycache__", "**/__pycache__/*", // .pytest_cache "**/pytest_cache/*", ".pytest_cache/*", "pytest_cache/*", "**/pytest_cache", ".pytest_cache", "pytest_cache", ]; function getGitIgnorePatterns(cwd) { const gitIgnorePath = path.join(cwd, ".gitignore"); if (existsSync(gitIgnorePath)) { return { exclude: gitignore .parse(readFileSync(gitIgnorePath)) .patterns.filter((pattern) => !pattern.startsWith("!")), reinclude: gitignore .parse(readFileSync(gitIgnorePath)) .patterns.filter((pattern) => pattern.startsWith("!")) .map((pattern) => pattern.slice(1)), }; } return { exclude: [], reinclude: [] }; } function getAllGitIgnorePatterns(cwd) { const patterns = getGitIgnorePatterns(cwd); readdirSync(cwd, { withFileTypes: true }).forEach((file) => { if (file.isDirectory() && !file.name.startsWith(".") && !file.name.startsWith("node_modules")) { const newPatterns = getAllGitIgnorePatterns(path.join(cwd, file.name)); patterns.exclude = [ ...patterns.exclude, ...newPatterns.exclude.map((pattern) => path.join(path.relative(cwd, path.join(cwd, file.name)), pattern)), ]; patterns.reinclude = [ ...patterns.reinclude, ...newPatterns.reinclude.map((pattern) => path.join(path.relative(cwd, path.join(cwd, file.name)), pattern)), ]; } }); return patterns; } // Upload the project code to S3 for in-browser editing export async function uploadUserCode(name, region, stage, cwd) { const tmpFolderProject = await createTemporaryFolder(); debugLogger.debug(`Creating archive of the project in ${tmpFolderProject}`); const { exclude, reinclude } = getAllGitIgnorePatterns(cwd); const promiseZip = zipDirectory(cwd, path.join(tmpFolderProject, "projectCode.zip"), true, excludedFiles.concat(exclude), reinclude); await promiseZip; const presignedUrlForProjectCode = await getPresignedURLForProjectCodePush(region, name, stage); return uploadContentToS3(presignedUrlForProjectCode, path.join(tmpFolderProject, "projectCode.zip")); }