genezio
Version:
Command line utility to interact with Genezio infrastructure.
884 lines (883 loc) • 40.1 kB
JavaScript
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"));
}