@webda/shell
Version:
Deploy a Webda app or configure it
383 lines • 16.2 kB
JavaScript
import { getCommonJS, JSONUtils } from "@webda/core";
import fs from "fs-extra";
import { globSync } from "glob";
import * as path from "path";
import { Packager } from "../index.js";
import { Deployer } from "./deployer.js";
const { __dirname } = getCommonJS(import.meta.url);
/**
* Predefined containerd client commands
*/
export const ClientDefinitions = {
docker: {
buildFile: "docker build --file ${file} .",
buildTagFile: "docker build --tag ${tag} --file ${file} .",
buildTagStdin: "docker build --tag ${tag} --file - .",
buildStdin: "docker build --file - .",
pushTag: "docker push ${tag}"
},
buildah: {
buildFile: "buildah bud --format=docker -f ${file} .",
buildTagFile: "buildah bud --format=docker -f ${file} -t ${tag} .",
buildTagStdin: "cat | buildah bud --format=docker -f - -t ${tag} .",
buildStdin: "cat | buildah bud --format=docker -f - .",
pushTag: "buildah push ${tag}"
}
};
/**
* @WebdaDeployer WebdaDeployer/Container
*/
export class Container extends Deployer {
constructor() {
super(...arguments);
this._copied = false;
this.workspaces = false;
}
async loadDefaults() {
await super.loadDefaults();
this.resources.baseImage = this.resources.baseImage || "docker.io/library/node:lts-alpine";
this.resources.command = this.resources.command || "serve";
this.resources.excludePackages = this.resources.excludePackages || [];
this.resources.containerClient = this.resources.containerClient || "docker";
if (typeof this.resources.containerClient == "string") {
if (!ClientDefinitions[this.resources.containerClient]) {
throw new Error(`Client profile '${this.resources.containerClient}' does not exist for ContainerClient`);
}
this.resources.containerClient = ClientDefinitions[this.resources.containerClient];
}
this.client = this.resources.containerClient;
if (!this.resources.workDirectory &&
this.resources.includeWorkspaces &&
this.getApplication().getPackageWebda().workspaces) {
let workspacePath = this.getApplication().getPackageWebda().workspaces.path;
if (workspacePath) {
this.workspaces = true;
this.logger.log("INFO", `Workspaces detected using ${workspacePath} as workingDirectory`);
this.resources.workDirectory = workspacePath;
}
}
}
/**
* Build a Docker image with webda application
*
* @param tag to build
* @param file path of Dockerfile
* @param command webda command to run
*/
async buildContainer(tag, file) {
let args = {};
let stdin;
let cmd;
if (tag) {
args.tag = tag;
}
if (file) {
args.file = file;
stdin = null;
cmd = tag ? this.client.buildTagFile : this.client.buildFile;
}
else {
if (this.workspaces) {
stdin = this.getWorkspacesDockerfile();
}
else {
stdin = this.getDockerfile();
}
cmd = tag ? this.client.buildTagStdin : this.client.buildStdin;
}
this.logger.log("INFO", `Launching Docker build ${this.replaceArgs(cmd, args)}`);
return this.execute(this.replaceArgs(cmd, args), stdin, false, "INFO");
}
/**
* Replace ${...} arguments within a string
*
* `docker build --tag ${tag} --file ${file}`
* will be replace by
* `docker build --tag mytag:1.2.3` --file /tmp/plop`
*
* @param cmd to be executed after
* @param args map to replace
* @returns
*/
replaceArgs(cmd, args) {
for (let i in args) {
cmd = cmd.replace(new RegExp("\\$\\{" + i + "\\}", "g"), args[i]);
}
return cmd;
}
/**
* Create Docker image and push
*/
async deploy() {
let { tag, push, Dockerfile } = this.resources;
let cwd = process.cwd();
try {
process.chdir(this.resources.workDirectory || cwd);
if (this.resources.includeLinkModules) {
this.logger.log("INFO", `Copy linked modules into link_modules`);
fs.emptyDirSync("link_modules");
}
await this.buildContainer(tag, Dockerfile);
if (tag && push) {
this.logger.log("INFO", `Pushing image ${tag}`);
// Push
await this.execute(this.replaceArgs(this.client.pushTag, { tag }));
}
this.logger.log("INFO", `Docker deployment finished`);
}
finally {
process.chdir(cwd);
}
return { tag };
}
copyPackageToLinkModules(pkg, includeModules = false, _subpkg = "") {
if (fs.realpathSync(pkg).startsWith(process.cwd())) {
// We should not copy package that will be in the Docker context
return;
}
this.logger.log("INFO", "Copying", pkg, "to linked modules");
let packageInfo = Packager.loadPackageInfo(pkg);
let includes = packageInfo.files || ["lib"];
includes.push("package.json");
if (includeModules) {
includes.push("node_modules");
}
includes.forEach(p => {
let includeDir = path.join(pkg, p);
globSync(includeDir).forEach(src => {
let rel = path.relative(pkg, src);
this.logger.log("INFO", "Copying", src, `link_modules/${packageInfo.name}/${rel}`);
fs.copySync(src, `link_modules/${packageInfo.name}/${rel}`, {
filter: f => {
// Do not copy symbolic link as they seems to pose problem
return !fs.lstatSync(f).isSymbolicLink();
}
});
});
});
this.scanLinkModules(pkg, src => {
this.logger.log("INFO", "COPYING LINKED MODULE", src);
this.copyPackageToLinkModules(src, true, pkg);
});
}
scanLinkModules(absPath, onLinkModule) {
let nodeModulesDir = path.join(absPath, "node_modules");
const checkDir = modulesDir => {
if (fs.existsSync(modulesDir)) {
fs.readdirSync(modulesDir).forEach(f => {
let stat = fs.lstatSync(path.join(modulesDir, f));
if (stat.isSymbolicLink()) {
onLinkModule(fs.realpathSync(path.join(modulesDir, f)), path.relative(nodeModulesDir, path.join(modulesDir, f)));
}
if (f.startsWith("@") && stat.isDirectory()) {
checkDir(path.join(modulesDir, f));
}
});
}
};
checkDir(nodeModulesDir);
}
/**
* Return the instruction to add webda-shell in Docker
*
* If within development repository it will copy all local files
* Otherwise just a simple yarn add
*/
getDockerfileWebdaShell() {
// If version is enforced
if (process.env.WEBDA_SHELL_DEPLOY_VERSION) {
return `# Install enforced /shell version\nRUN yarn -W add /shell@${process.env["WEBDA_SHELL_DEPLOY_VERSION"]}\n\n`;
}
// If version is set to dev
if (process.env.WEBDA_SHELL_DEV) {
let dockerfile = "# Use development Webda Shell version\n";
this.logger.log("INFO", `Development version of /shell (WEBDA_SHELL_DEV=${process.env.WEBDA_SHELL_DEV})`);
// Copy webda-shell into build directory
let sign = "";
if (fs.existsSync(".webda-shell/hash")) {
sign = fs.readFileSync(".webda-shell/hash").toString();
}
let currentSign = Packager.getPackageLastChanges(path.join(__dirname, "../.."), true);
if (currentSign !== sign) {
this.logger.log("INFO", "Updating @webda/shell version as development version is different");
fs.emptyDirSync(".webda-shell");
// Prevent to copy if in webda repo - only useful for test
/* c8 ignore next 3 */
if (path.relative(path.resolve(path.join(__dirname, "../../../..")), process.cwd()) !== "packages/shell") {
fs.copySync(path.join(__dirname, "../../../.."), ".webda-shell");
}
fs.writeFileSync(".webda-shell/hash", currentSign);
}
dockerfile += `ADD .webda-shell /devshell
ADD .webda-shell/node_modules /devshell/node_modules/
ADD .webda-shell/node_modules /webda/node_modules/
ENV PATH=\${PATH}:/devshell/packages/shell/bin\n`;
return dockerfile + "\n";
}
// Normal take the same version as local webda-shell
let tag = JSONUtils.loadFile(__dirname + "/../../package.json").version;
return `# Install current /shell version\nRUN yarn -W add /shell@${tag}\n\n`;
}
getDockerfileHeader() {
return `FROM ${this.resources.baseImage}
LABEL webda.io/deployer=${this.name}
LABEL webda.io/deployment=${this.manager.getDeploymentName()}
LABEL webda.io/version=${this.getApplication().getWebdaVersion()}
LABEL webda.io/application=${this.getApplication().getPackageDescription().name}
LABEL webda.io/application/version=${this.getApplication().getPackageDescription().version}
EXPOSE 18080
RUN mkdir -p /webda
WORKDIR /webda
ADD package.json /webda/\n\n`;
}
getWorkspacesDockerfile() {
let appPath = this.manager.getApplication().getAppPath();
let relPath = path.relative(process.cwd(), appPath);
let dockerfile = this.getDockerfileHeader();
let packages = Packager.getWorkspacesPackages();
packages.forEach(pack => {
dockerfile += `ADD ${pack}/package.json /webda/${pack}/package.json\n`;
});
dockerfile += `RUN yarn install --production\n\n`;
dockerfile += `# Copy all packages content\n`;
packages.forEach(pack => {
if (this.resources.excludePackages.indexOf(pack) >= 0) {
return;
}
this.logger.log("INFO", "Include package", pack);
dockerfile += this.copyPackageFilesTo(pack, `/webda/${pack}`,
// Add webda.config.json on target package
relPath === pack ? ["webda.config.json"] : undefined);
});
dockerfile += "\n";
if (this.resources.includeLinkModules) {
dockerfile += "# Add link modules to node_modules\n";
this.scanLinkModules(process.cwd(), (src, rel) => {
if (!src.startsWith(process.cwd())) {
let root = Packager.getWorkspacesRoot(src);
if (root) {
this.logger.log("INFO", "Copying linked package workspace", rel);
// Copy all workspace packages
Packager.getWorkspacesPackages(root).forEach(p => {
let pPath = path.join(root, p);
if (fs.existsSync(path.join(pPath, "package.json"))) {
let name = Packager.loadPackageInfo(pPath).name;
this.logger.log("INFO", "Copying linked package workspace deps", name);
dockerfile += `RUN rm -rf /webda/node_modules/${name}\n`;
this.copyPackageToLinkModules(pPath);
}
});
}
else {
this.logger.log("INFO", "Copying linked package", rel);
// Remove current package
dockerfile += `RUN rm -rf /webda/node_modules/${rel}\n`;
this.copyPackageToLinkModules(src);
}
}
});
dockerfile += `ADD link_modules /webda/node_modules\n\n`;
}
// Include webda-shell
dockerfile += this.getDockerfileWebdaShell();
// Lerna operation finished
dockerfile += `# Update WORKDIR to project\nWORKDIR ${path.join("/webda", relPath)}\n\n`;
// Add deployment
dockerfile += this.addDeploymentToImage(path.join(relPath, "deployments"), path.join("/webda", relPath));
// Add commmand
dockerfile += this.addCommandToImage();
if (this.resources.debugDockerfilePath) {
fs.writeFileSync(this.resources.debugDockerfilePath, dockerfile);
}
return dockerfile;
}
/**
* Add the deployment export
* @param localPath
* @param appPath
* @returns
*/
addDeploymentToImage(localPath = "deployments", appPath = "/webda/") {
let deployment = this.manager.getDeploymentName();
let gitInfo = Buffer.from(JSON.stringify(this.app.getGitInformation())).toString("base64");
if (deployment) {
// Export deployment
return `# Add deployment
COPY ${localPath} ${path.join(appPath, "deployments")}
RUN GIT_INFO=${gitInfo} /webda/node_modules/.bin/webda -d ${deployment} config --noCompile webda.config.json${fs.existsSync(this.getApplication().getAppPath("webda.config.jsonc")) ? "c" : ""}
RUN rm -rf deployments\n\n`;
}
return "";
}
copyPackageFilesTo(pkg, dst, addFiles = []) {
let absPath = path.resolve(pkg);
let packageInfo = Packager.loadPackageInfo(absPath);
let includes = packageInfo.files || ["lib"];
addFiles.forEach(f => {
if (includes.indexOf(f) < 0) {
includes.push(f);
}
});
let dockerfile = `# Package ${pkg}\n`;
includes.forEach(p => {
if (fs.existsSync(path.join(pkg, p))) {
dockerfile += `ADD ${path.join(pkg, p)} ${path.join(dst, p)}\n`;
}
});
// Link modules
if (this.resources.includeLinkModules) {
let links = "";
// Check for level1 and 2 symlink
this.scanLinkModules(absPath, (src, rel) => {
if (!src.startsWith(process.cwd())) {
// Remove current package
links += `RUN rm -rf ${dst}/node_modules/${rel} && rm -rf /webda/node_modules/${rel}\n`;
this.copyPackageToLinkModules(src);
}
});
if (links.length) {
dockerfile += `# Linked packages to ${pkg}\n${links}`;
}
}
return dockerfile;
}
addCommandToImage() {
let { command, logFile, errorFile } = this.resources;
if (logFile) {
logFile = " > " + logFile;
}
else {
logFile = "";
}
if (errorFile) {
errorFile = " 2> " + errorFile;
}
else {
errorFile = "";
}
return `# Change user
USER 1000
# Launch webda\nENV WEBDA_COMMAND='${command}'
CMD /webda/node_modules/.bin/webda --noCompile $WEBDA_COMMAND ${logFile} ${errorFile}\n\n`;
}
/**
* Generate a dynamic Dockerfile with webda application
*/
getDockerfile() {
let dockerfile = this.getDockerfileHeader() + "RUN yarn install --production\n\n";
dockerfile += `ENV PATH "$PATH:./node_modules/.bin/"`;
dockerfile += this.copyPackageFilesTo(".", "/webda", ["webda.config.json", "webda.config.jsonc"]);
// Import webda-shell
dockerfile += this.getDockerfileWebdaShell();
// Add deployment
dockerfile += this.addDeploymentToImage("deployments", "/webda");
// Add the packager date
dockerfile += "RUN date > .webda.packaged\n";
dockerfile += this.addCommandToImage();
if (this.resources.debugDockerfilePath) {
fs.writeFileSync(this.resources.debugDockerfilePath, dockerfile);
}
return dockerfile;
}
}
//# sourceMappingURL=container.js.map