genezio
Version:
Command line utility to interact with Genezio infrastructure.
202 lines (201 loc) • 9.14 kB
JavaScript
import { $ } from "execa";
import git from "isomorphic-git";
import fs from "fs";
import { UserError } from "../../../errors.js";
import { debugLogger, log } from "../../../utils/logging.js";
import { actionDetectedEnvFile, prepareServicesPostBackendDeployment, prepareServicesPreBackendDeployment, readOrAskConfig, createBackendEnvVarList, } from "../utils.js";
import { FunctionConfiguration, ProjectConfiguration, } from "../../../models/projectConfiguration.js";
import { CloudProviderIdentifier } from "../../../models/cloudProviderIdentifier.js";
import { getCloudProvider } from "../../../requests/getCloudProvider.js";
import { getCloudAdapter } from "../genezio.js";
import { GenezioCloudInputType, GenezioFunctionMetadataType, } from "../../../cloudAdapter/cloudAdapter.js";
import { FunctionType } from "../../../projectConfiguration/yaml/models.js";
import { createTemporaryFolder } from "../../../utils/file.js";
import path from "path";
import { reportSuccessFunctions } from "../../../utils/reporter.js";
import { addContainerComponentToConfig } from "./utils.js";
import { statSync } from "fs";
import colors from "colors";
import { DASHBOARD_URL } from "../../../constants.js";
import { ContainerComponentType } from "../../../models/projectOptions.js";
import { warningMissingEnvironmentVariables } from "../../../utils/environmentVariables.js";
import { isCI } from "../../../utils/process.js";
export async function dockerDeploy(options) {
const config = await readOrAskConfig(options.config);
if (!config.container) {
const relativePath = path.relative(process.cwd(), options.image || process.cwd()) || ".";
await addContainerComponentToConfig(options.config, config, {
path: relativePath,
});
}
// Give the user another chance if he forgot to add `--env` flag
if (!isCI() && !options.env) {
options.env = await actionDetectedEnvFile(config.container?.path || ".", config.name, options.stage);
}
// Prepare services before deploying (database, authentication, etc)
await prepareServicesPreBackendDeployment(config, config.name, options.stage, options.env);
const projectConfiguration = new ProjectConfiguration(config, CloudProviderIdentifier.GENEZIO_CLOUD, {
generatorResponses: [],
classesInfo: [],
});
const containerPath = config.container?.path || process.cwd();
let dockerfile = "Dockerfile";
let cwd = ".";
if (statSync(containerPath).isDirectory()) {
cwd = containerPath;
}
else {
dockerfile = path.basename(containerPath);
cwd = path.dirname(containerPath);
}
log.info("Check docker version...");
await $({ stdio: "inherit" }) `docker --version`.catch((err) => {
debugLogger.error(err);
throw new UserError("Docker is not installed. Please install Docker and try again.");
});
log.info("Building image...");
await $({
stdin: "inherit",
stderr: "inherit",
cwd,
}) `docker buildx build --platform=linux/amd64 -t ${config.name} -f ${dockerfile} .`.catch((err) => {
debugLogger.error(err);
throw new UserError(`Failed to build Docker image. Error: ${err}`);
});
log.info("Creating the container...");
const { stdout } = await $ `docker create --name genezio-${config.name} ${config.name}`.catch((err) => {
debugLogger.error(err);
throw new UserError("Failed to create the container.");
});
const containerId = await extractContainerId(stdout);
const tempFolder = await createTemporaryFolder();
const archivePath = path.join(tempFolder, `genezio-${config.name}.tar`);
log.info("Exporting the container...");
await $({
stdio: "inherit",
shell: true,
cwd,
}) `docker export genezio-${config.name} > ${archivePath}`.catch((err) => {
debugLogger.error(err);
throw new UserError("Failed to export the container.");
});
const { stdout: stdoutInspect } = await $ `docker inspect ${containerId}`.catch((err) => {
debugLogger.error(err);
throw new UserError("Failed to inspect the container.");
});
await $({ stdio: "inherit" }) `docker container rm genezio-${config.name}`.catch((err) => {
debugLogger.error(err);
throw new UserError("Failed to remove the container.");
});
const inspectResult = JSON.parse(stdoutInspect);
const envs = inspectResult[0].Config.Env;
const cmd = inspectResult[0].Config.Cmd;
const dockerWorkingDir = inspectResult[0].Config.WorkingDir;
const entrypoint = inspectResult[0].Config.Entrypoint;
const exposedPorts = inspectResult[0].Config.ExposedPorts;
let cmdEntryFile = "";
const port = getPort(exposedPorts);
if (entrypoint) {
cmdEntryFile += entrypoint.join(" ") + " ";
}
else {
cmdEntryFile += "/bin/sh -c ";
}
if (cmd) {
// Always quote the command when using /bin/sh -c to ensure proper argument handling
cmdEntryFile += `'${cmd.join(" ")}'`;
}
projectConfiguration.functions.push(new FunctionConfiguration(
/*name:*/ "docker-container",
/*path:*/ "",
/*handler:*/ "",
/*language:*/ "container",
/*entry*/ "",
/*type:*/ FunctionType.aws,
/*timeout:*/ undefined,
/*storageSize:*/ undefined,
/*instanceSize:*/ undefined,
/*vcpuCount:*/ undefined,
/*memoryMb:*/ undefined,
/*maxConcurrentRequestsPerInstance:*/ undefined,
/*maxConcurrentInstances:*/ undefined,
/*cooldownTime:*/ undefined,
/*persistent:*/ undefined,
/*healthcheckPath:*/ config.container?.healthcheckPath));
const environmentVariables = await createBackendEnvVarList(options.env, options.stage, config, ContainerComponentType.container);
const envVars = envs.map((env) => {
const components = env.split("=");
const key = components[0];
const value = components.slice(1).join("=");
return {
name: key,
value,
};
});
environmentVariables.push(...envVars);
const projectGitRepositoryUrl = (await git.listRemotes({ fs, dir: process.cwd() })).find((r) => r.remote === "origin")?.url;
const cloudProvider = await getCloudProvider(projectConfiguration.name);
const cloudAdapter = getCloudAdapter(cloudProvider);
const metadata = {
type: GenezioFunctionMetadataType.Container,
cmd: cmdEntryFile,
cwd: dockerWorkingDir,
http_port: port,
healthcheck_path: config.container?.healthcheckPath,
};
const result = await cloudAdapter.deploy([
{
type: GenezioCloudInputType.FUNCTION,
name: "docker-container",
archivePath,
archiveName: `genezio-${config.name}.tar`,
entryFile: cmdEntryFile,
unzippedBundleSize: 100,
metadata: metadata,
timeout: config.container.timeout,
storageSize: config.container.storageSize,
instanceSize: config.container.instanceSize,
vcpuCount: config.container.vcpuCount,
memoryMb: config.container.memoryMb,
maxConcurrentRequestsPerInstance: config.container.maxConcurrentRequestsPerInstance,
maxConcurrentInstances: config.container.maxConcurrentInstances,
cooldownTime: config.container.cooldownTime,
persistent: config.container?.type === FunctionType.persistent,
},
], projectConfiguration, {
stage: options.stage,
}, ["docker"],
/* sourceRepository */ projectGitRepositoryUrl,
/* environmentVariables */ environmentVariables);
await warningMissingEnvironmentVariables(config.container?.path || "./", result.projectId, result.projectEnvId);
// Prepare services after deploying (authentication redirect urls)
await prepareServicesPostBackendDeployment(config, config.name, options.stage);
reportSuccessFunctions(result.functions);
log.info(`\nApp Dashboard URL: ${colors.cyan(`${DASHBOARD_URL}/project/${result.projectId}/${result.projectEnvId}`)}\n` +
`${colors.dim("Here you can monitor logs, set up a custom domain, and more.")}\n`);
}
function getPort(exposedPort) {
let port = "8080";
if (exposedPort && Object.keys(exposedPort).length >= 2) {
throw new UserError("Only one port can be exposed.");
}
else if (exposedPort && Object.keys(exposedPort).length === 1) {
port = Object.keys(exposedPort)[0].split("/")[0];
const protocol = Object.keys(exposedPort)[0].split("/")[1];
if (protocol !== "tcp") {
throw new UserError("Only TCP protocol is supported.");
}
}
return port;
}
async function extractContainerId(stdout) {
// Use a regular expression to match a 64-character hexadecimal string
const containerIdMatch = stdout.match(/[a-f0-9]{64}/i);
// If a match is found, return the first match (container ID)
if (containerIdMatch) {
return containerIdMatch[0];
}
else {
throw new Error("Container ID not found in the output.");
}
}