diff --git a/package-lock.json b/package-lock.json index 36b392be..668dd8fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -479,6 +479,15 @@ "@types/express": "*" } }, + "@types/dockerode": { + "version": "2.5.34", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.34.tgz", + "integrity": "sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -1053,6 +1062,11 @@ } } }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1061,6 +1075,33 @@ "tweetnacl": "^0.14.3" } }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1157,6 +1198,15 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -1267,6 +1317,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1667,6 +1722,51 @@ "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", "dev": true }, + "docker-modem": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.4.tgz", + "integrity": "sha512-vDTzZjjO1sXMY7m0xKjGdFMMZL7vIUerkC3G4l6rnrpOET2M6AOufM8ajmQoOB+6RfSn6I/dlikCUq/Y91Q1sQ==", + "requires": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^0.8.7" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "dockerode": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.1.tgz", + "integrity": "sha512-XsSVB5Wu5HWMg1aelV5hFSqFJaKS5x1aiV/+sT7YOzOq1IRl49I/UwV8Pe4x6t0iF9kiGkWu5jwfvbkcFVupBw==", + "requires": { + "docker-modem": "^2.1.0", + "tar-fs": "~2.0.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2467,6 +2567,11 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3266,6 +3371,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "3.3.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", @@ -4588,6 +4698,11 @@ } } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -6138,6 +6253,11 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" }, + "split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -6152,6 +6272,24 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "ssh2": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", + "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "requires": { + "ssh2-streams": "~0.4.10" + } + }, + "ssh2-streams": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", + "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "requires": { + "asn1": "~0.2.0", + "bcrypt-pbkdf": "^1.0.2", + "streamsearch": "~0.1.2" + } + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -6206,6 +6344,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -6269,7 +6412,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6339,6 +6481,41 @@ } } }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", + "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "test-exclude": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", @@ -6644,8 +6821,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", diff --git a/package.json b/package.json index b1c6d8ac..2fe6d1d1 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/node-fetch": "^2.5.0", "@types/uuid": "^3.4.5", "cookie-parser": "^1.4.4", + "dockerode": "^3.2.1", "dotenv": "^8.1.0", "ejs": "^2.6.2", "express": "^4.17.1", @@ -48,6 +49,7 @@ "uuid": "^3.3.3" }, "devDependencies": { + "@types/dockerode": "^2.5.34", "@types/jest": "^24.0.17", "@types/node": "^12.7.1", "@typescript-eslint/eslint-plugin": "^2.0.0", diff --git a/scripts/post-receive b/scripts/post-receive index 2a196778..44bb362e 100755 --- a/scripts/post-receive +++ b/scripts/post-receive @@ -1,9 +1,12 @@ #!/bin/sh cd .. GIT_DIR='.git' +DOMAIN=$(basename $(pwd)) umask 002 && git reset --hard git clean -f git checkout master git branch -D prod npm install -sudo -u myproxy pm2 startOrRestart deploy.config.js --env production --update-env +echo "Starting the container..." +curl --silent --unix-socket /var/run/docker.sock -X POST "http:/localhost/containers/${DOMAIN}/restart" +echo "Your app is running at https://${DOMAIN}" diff --git a/scripts/setup.sh b/scripts/setup.sh index 2ee97234..446415a7 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,12 +1,35 @@ #!/bin/bash -if ! command node -v &>/dev/null; then +# Helper functions +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +user_exists() { + id "$1" &> /dev/null +} + +# Check if docker is installed and stop the script if it's not +if ! command_exists docker; then + echo "myProxy requires Docker to run" + echo "Docker installation instructions: https://docs.docker.com/engine/install/" + exit +fi + +if ! command_exists node; then + echo "Installing node" sudo apt-get install curl curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install -y nodejs fi + +if ! command_exists pm2; then + echo "Installing pm2" + npm install pm2 -g +fi + npm install -npm install pm2 -g + if [ ! -d "./acme.sh" ] ; then git clone https://github.com/Neilpang/acme.sh.git cd ./acme.sh @@ -14,36 +37,64 @@ if [ ! -d "./acme.sh" ] ; then ./acme.sh --upgrade --auto-upgrade cd ../ fi -if [ ! -d "/home/myproxy" ] ; then - # Add users + +sudo groupadd -f docker + +if ! user_exists myproxy; then + echo "Creating user: myproxy" sudo useradd -m -c "myproxy" myproxy -s /bin/bash -p $(echo $ADMIN | openssl passwd -1 -stdin) -d "/home/myproxy" + sudo usermod -aG docker myproxy + mkdir -p /home/myproxy/.ssh + mkdir -p /home/myproxy/.scripts +fi + +if ! user_exists git; then + echo "Creating user: git" sudo useradd -m -G myproxy -s $(which git-shell) -p $(echo $ADMIN | openssl passwd -1 -stdin) git - # Add sudoers rule for git user to run pm2 as myproxy, without password - echo "git ALL = (myproxy) NOPASSWD: /usr/bin/pm2" > /etc/sudoers.d/git - # Create folders - mkdir /home/myproxy/.ssh - mkdir /home/git/.ssh - mkdir /home/myproxy/.scripts - # Copy ssh keys and scripts - cp ~/.ssh/authorized_keys /home/myproxy/.ssh/authorized_keys - cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys - cp ./scripts/post-receive /home/myproxy/.scripts/post-receive - cp ./scripts/pre-receive /home/myproxy/.scripts/pre-receive - cp ./scripts/gitignore /home/myproxy/.scripts/.gitignore + sudo usermod -aG docker git + mkdir -p /home/git/.ssh # Disable SSH MOTD message for git user touch /home/git/.hushlogin # Add git-shell message - mkdir /home/git/git-shell-commands + mkdir -p /home/git/git-shell-commands cp ./scripts/no-interactive-login /home/git/git-shell-commands/no-interactive-login chmod +x /home/git/git-shell-commands/no-interactive-login - # fix file permissions - chown myproxy:myproxy -R /home/myproxy/ - chown git:git -R /home/git/ - chmod 2775 -R /home/myproxy/ +fi + +if [ -f "~/.ssh/authorized_keys" ]; then + cp ~/.ssh/authorized_keys /home/myproxy/.ssh/authorized_keys + cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys # Prepend ssh options for authorized keys sed -i '/^ssh-rsa/s/^/no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty /' /home/git/.ssh/authorized_keys +else + touch /home/myproxy/.ssh/authorized_keys + touch /home/git/.ssh/authorized_keys fi + +cp ./scripts/post-receive /home/myproxy/.scripts/post-receive +cp ./scripts/pre-receive /home/myproxy/.scripts/pre-receive +cp ./scripts/gitignore /home/myproxy/.scripts/.gitignore + +# fix file permissions +chown myproxy:myproxy -R /home/myproxy/ +chown git:git -R /home/git/ +# set the group permissions for /home/myproxy +# 2 = set the setgid bit for the files so group permissions are inherited +# 775 = set read+write permissions for the user and group +chmod 2775 -R /home/myproxy/ + npm run build + if [ ! -f "./data.db" ] ; then touch data.db fi + +# pull node docker image +if docker ps > /dev/null 2>&1; then + docker pull node:alpine +else + echo "WARNING: Couldn't run docker commands" + echo "WARNING: Make sure your user has the right permissions" + echo "WARNING: Go to this link to setup docker to run without root" + echo "WARNING: https://docs.docker.com/engine/install/linux-postinstall/" +fi \ No newline at end of file diff --git a/src/api/logs.ts b/src/api/logs.ts index f5f1d616..7c7d6de9 100644 --- a/src/api/logs.ts +++ b/src/api/logs.ts @@ -1,65 +1,37 @@ import express from 'express' -import fs from 'fs' import environment from '../helpers/environment' import { getMappingByDomain } from '../lib/data' +import { getContainerLogs } from '../helpers/docker' const logsRouter = express.Router() const { isProduction } = environment -logsRouter.get('/err/:domain', (req, res) => { - const { domain } = req.params +logsRouter.get('/:stream/:domain', async (req, res) => { + const { stream, domain } = req.params + const { follow, tail } = req.query - if (isProduction()) { - // Only search for domain when running in production. The test does not - // require a valid domain since it only verifies the endpoint - const { fullDomain } = getMappingByDomain(domain) - // Pipes the error log files to res - res.setHeader('content-type', 'text/plain') - fs.createReadStream(`/home/myproxy/.pm2/logs/${fullDomain}-err.log`).pipe( - res - ) - } else { - res.send('OK') + // Stream validation + if (stream !== 'stdout' && stream !== 'stderr') { + return res + .status(400) + .json({ message: 'stream param must be stdout or stderr' }) } -}) - -logsRouter.get('/out/:domain', (req, res) => { - const { domain } = req.params if (isProduction()) { // Only search for domain when running in production. The test does not // require a valid domain since it only verifies the endpoint const { fullDomain } = getMappingByDomain(domain) - // Pipes the output log file to res. Console.Log from your app will appear here + // Pipes the log to res res.setHeader('content-type', 'text/plain') - fs.createReadStream(`/home/myproxy/.pm2/logs/${fullDomain}-out.log`).pipe( - res - ) + const logStream = await getContainerLogs(fullDomain, { + follow: Boolean(follow), + tail: Number(tail), + [stream]: true + }) + logStream.pipe(res) } else { res.send('OK') } }) -logsRouter.delete('/:domain', (req, res) => { - const { domain } = req.params - - if (isProduction()) - fs.writeFile( - `/home/myproxy/.pm2/logs/${domain}-out.log`, - 'Log cleared\n', - err => { - if (err) console.log('Error deleting output log') - } - ) - fs.writeFile( - `/home/myproxy/.pm2/logs/${domain}-err.log`, - 'Log cleared\n', - err => { - if (err) console.log('Error deleting error log') - } - ) - - res.send('LOGS DELETED') -}) - export default logsRouter diff --git a/src/api/mapping.ts b/src/api/mapping.ts index 66394282..2b84be3e 100644 --- a/src/api/mapping.ts +++ b/src/api/mapping.ts @@ -11,9 +11,15 @@ import { deleteDomain } from '../lib/data' import { Mapping } from '../types/general' -import prodConfigure from '../../scripts/prod.config.js' import { getGitUserId, getGitGroupId } from '../helpers/getGitUser' import environment from '../helpers/environment' +import { + getContainersList, + createContainer, + startContainer, + stopContainer, + removeContainer +} from '../helpers/docker' const mappingRouter = express.Router() const exec = util.promisify(cp.exec) const getNextPort = (map, start = 3002): number => { @@ -42,25 +48,19 @@ mappingRouter.post('/', async (req, res) => { return acc }, {}) const portCounter = getNextPort(map) - const prodConfigApp = [...prodConfigure.apps][0] - prodConfigApp.name = fullDomain - prodConfigApp.env_production.PORT = parseInt(req.body.port || portCounter, 10) - prodConfigApp.script = `npm run start:myproxy << /home/myproxy/.pm2/logs/${fullDomain}-out.log` - prodConfigApp.error_file = `/home/myproxy/.pm2/logs/${fullDomain}-err.log` - prodConfigApp.merge_logs = true - delete prodConfigApp.env_production.ADMIN - const prodConfig = { - apps: prodConfigApp - } + const port = parseInt(req.body.port || portCounter, 10) const scriptPath = '.scripts' + // Create a new container and get the id + const id = isProduction() ? await createContainer(fullDomain, port) : uuid4() + const respond = (): void => { const mappingObject: Mapping = { domain: req.body.domain.toLowerCase(), subDomain: req.body.subDomain.toLowerCase(), - port: req.body.port || `${portCounter}`, + port: port.toString(), ip: req.body.ip || '127.0.0.1', - id: uuid4(), + id, gitLink: `git@${req.body.domain}:${WORKPATH}/${fullDomain}`, fullDomain } @@ -89,7 +89,6 @@ mappingRouter.post('/', async (req, res) => { cd ${fullDomain} git config user.email "root@ipaddress" git config user.name "user" - echo 'module.exports = ${JSON.stringify(prodConfig)}' > deploy.config.js git add . git commit -m "Initial Commit" `, @@ -103,16 +102,15 @@ mappingRouter.post('/', async (req, res) => { mappingRouter.get('/', async (req, res) => { const domains = getMappings() + if (!isProduction()) return res.json(domains.map(el => ({ ...el, status: 'not started' }))) - const data = await exec('su - myproxy -c "pm2 jlist"') - - const outArr = data.stdout.split('\n') - const statusData = JSON.parse(outArr[outArr.length - 1]).reduce( + const data = await getContainersList() + const statusData = data.reduce( (statusObj, el) => ({ ...statusObj, - [el.name]: el.pm2_env.status + [el.Names[0].replace('/', '')]: el.State }), {} ) @@ -132,23 +130,18 @@ mappingRouter.delete('/:id', async (req, res) => { deleteDomain(deletedDomain.fullDomain) if (!isProduction()) return res.json(deletedDomain) - // get user and group id to execute the commands with the correct permissions - const gitUserId = await getGitUserId() - const gitGroupId = await getGitGroupId() - - exec( - ` - cd ${WORKPATH} - export PM2_HOME=/home/myproxy/.pm2 - if command ls ${deletedDomain.fullDomain} | grep "package.json" &>/dev/null; then - pm2 delete ${deletedDomain.fullDomain} - fi - rm -rf ${deletedDomain.fullDomain} - `, - { uid: gitUserId, gid: gitGroupId } - ).then(() => { - res.json(deletedDomain) - }) + // stop and remove container + removeContainer(deletedDomain.fullDomain) + .then(() => { + // delete the domain folder + exec(` + cd ${WORKPATH} + rm -rf ${deletedDomain.fullDomain} + `).then(() => { + res.json(deletedDomain) + }) + }) + .catch(err => res.status(err.statusCode).json(err.json)) }) mappingRouter.get('/:id', (req, res) => { @@ -156,4 +149,18 @@ mappingRouter.get('/:id', (req, res) => { res.json(foundDomain || {}) }) +mappingRouter.get('/:id/start', (req, res) => { + const { id } = req.params + startContainer(id) + .then(() => res.sendStatus(204)) + .catch(err => res.status(err.statusCode).json(err.json)) +}) + +mappingRouter.get('/:id/stop', (req, res) => { + const { id } = req.params + stopContainer(id) + .then(() => res.sendStatus(204)) + .catch(err => res.status(err.statusCode).json(err.json)) +}) + export default mappingRouter diff --git a/src/helpers/docker.ts b/src/helpers/docker.ts new file mode 100644 index 00000000..9431acf1 --- /dev/null +++ b/src/helpers/docker.ts @@ -0,0 +1,96 @@ +import Docker from 'dockerode' +import path from 'path' +import { Readable, PassThrough } from 'stream' +import environment from '../helpers/environment' + +const docker = new Docker({ socketPath: '/var/run/docker.sock' }) + +const getContainersList = async (): Promise => { + const containers = await docker.listContainers({ all: true }) + return containers +} + +const getContainerLogs = async ( + id: string, + options: Docker.ContainerLogsOptions +): Promise => { + const container = docker.getContainer(id) + let logs = await container.logs(options) + if (!options.follow) { + // if follow = false, the logs are returned as a buffer + // so we need to convert into a stream + logs = Readable.from(logs) + } + const demuxedStream = new PassThrough() + container.modem.demuxStream(logs, demuxedStream, demuxedStream) + logs.on('end', () => demuxedStream.end()) + return demuxedStream +} + +const createContainer = async ( + fullDomain: string, + port: number +): Promise => { + const workPath = path.resolve(environment.WORKPATH, fullDomain) + return docker + .createContainer({ + Image: 'node:alpine', + name: fullDomain, + User: 'node', + ExposedPorts: { + '3000/tcp': {} + }, + Tty: false, + WorkingDir: '/home/node/app', + Env: ['NODE_ENV=production', 'PORT=3000'], + HostConfig: { + Binds: [`${workPath}:/home/node/app`], + RestartPolicy: { + Name: 'on-failure', + MaximumRetryCount: 3 + }, + LogConfig: { + Type: 'json-file', + Config: { + 'max-size': '10m', + 'max-file': '1' + } + }, + PortBindings: { + '3000/tcp': [ + { + HostIp: '', + HostPort: port.toString() + } + ] + } + }, + Cmd: ['node', '.'] + }) + .then(container => container.id) + .catch(err => err) +} + +const startContainer = async (id: string): Promise => { + const container = docker.getContainer(id) + return container.restart() +} + +const stopContainer = async (id: string): Promise => { + const container = docker.getContainer(id) + return container.stop() +} + +const removeContainer = async (id: string): Promise => { + const container = docker.getContainer(id) + return container.remove({ v: true, force: true }) +} + +export { + getContainersList, + getContainerLogs, + createContainer, + startContainer, + stopContainer, + removeContainer +} diff --git a/src/public/client.ts b/src/public/client.ts index 582b1e5c..318465c2 100644 --- a/src/public/client.ts +++ b/src/public/client.ts @@ -15,6 +15,10 @@ type Status = { status: string } +type ContainerResponse = { + message?: string +} + const create: HTMLElement = helper.getElement('.create') const hostSelector: HTMLElement = helper.getElement('#hostSelector') const domainList: HTMLElement = helper.getElement('.domainList') @@ -44,13 +48,11 @@ class MappingItem { // The variables below are to hide log related icons when pm2 is not // being used to monitor the apps. These apps will not have status since // they are not managed by pm2. - let settingClass let logClass - if (data.status === 'online') { + if (data.status === 'running') { iconClass = 'fa fa-circle mr-1 mt-1' iconColor = 'rgba(50,255,50,0.5)' logClass = 'fa fa-file-text-o ml-1 mt-1' - settingClass = 'ml-1 fa fa-cog' } else if (data.status === 'not started') { iconClass = '' iconColor = 'transparent' @@ -58,7 +60,6 @@ class MappingItem { iconClass = 'fa fa-circle mr-1 mt-1' iconColor = 'rgba(255, 50, 50, 0.5)' logClass = 'fa fa-file-text-o ml-1 mt-1' - settingClass = 'ml-1 fa fa-cog' } mappingElement.classList.add( 'list-group-item', @@ -80,34 +81,32 @@ class MappingItem { -
- - - - -
${data.gitLink} + +