#!/usr/bin/env -S /root/.deno/bin/deno run --allow-env --allow-read --allow-sys --allow-run --allow-write import { printf } from "https://deno.land/std@0.170.0/fmt/printf.ts"; import {brightGreen, brightMagenta} from "https://deno.land/std@0.170.0/fmt/colors.ts"; import { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts"; import { getLogger } from "https://deno.land/std@0.170.0/log/mod.ts"; class CommandError extends Error{} class ArgError extends CommandError {} class NginxError extends CommandError {} interface DeployedService{ name: string, port?: number, unixSocketPath?: string, } async function saveService(services: DeployedService[]){ const content = JSON.stringify(services); await Deno.writeTextFile("services.json", content); } async function loadService(): Promise { try { const content = await Deno.readTextFile("services.json"); return JSON.parse(content); } catch (error) { if(error instanceof Deno.errors.NotFound){ return []; } else{ throw error; } } } interface NginxConfigOption { name: string, /** * port number */ port?: number, /** * unix socket path */ unixSocket?: string, /** * megabyte unit */ clientMaxBodySize?: number, /** * proxy pass * @default "127.0.0.1" */ proxyPass?: string, /** * socket IO support * @default true */ socketIO?: boolean, } function createNginxConfigContent({ port, unixSocket, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) { clientMaxBodySize ??= 20; proxyPass ??= "127.0.0.1"; socketIO ??= true; if (!port && !unixSocket) { throw new ArgError("Both port and unixSocket cannot be false."); } if ((!!port) && (!!unixSocket)){ throw new ArgError("Both port and unixSocket cannot be true."); } if (!!port && (isNaN(port) || !isFinite(port) )){ throw new ArgError("Port must be number."); } if (!!port && (port > 65535 || port < 0)){ throw new ArgError("Port cannnot be less than 0 or greater than 65535") } if (!!unixSocket && !unixSocket.startsWith("/")){ throw new ArgError("unix socket path must be absolute path.") } const content = `# it created by deploy script. server { server_name ${name}.prelude.duckdns.org; location /{ proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-NginX-Proxy true; proxy_pass ${ port ? `http://${proxyPass}:${port}` : `http://unix:${unixSocket}/` }; proxy_redirect off; # client body size client_max_body_size ${clientMaxBodySize}M; ${ socketIO ? `# Socket.IO Support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";` : "" } } listen 80; }`; return content; } function isRunAsRoot() { return Deno.uid() == 0; } async function NginxCheck() { const c = new Deno.Command("nginx",{ args: ["-t"], }); const status = await c.output(); return status.success && status.code == 0; } async function NginxReload() { const c = new Deno.Command("nginx",{ args: ["-s", "reload"], }); const status = await c.output(); return status.success && status.code == 0; } interface DeployCommandOption{ port?: number; proxy?: string; disableSocket?: boolean; clientMaxBodySize?: number; unixSocket?: string; } function checkDomainName(name: string){ return /^[a-z0-9][a-z0-9_-]{0,61}[a-z0-9]?$/i.test(name); } async function deployCommand(deployName: string, options?: DeployCommandOption): Promise{ options ??= {} if(!checkDomainName(deployName)){ throw new ArgError("name invalid") } if(!isRunAsRoot()){ getLogger().warning("It's not executed as root") } const services = await loadService(); const dir = [...Deno.readDirSync("/etc/nginx/sites-available")] if (dir.map(x => x.name).includes(deployName)) { throw new ArgError("duplicated name: please use other name.") } const proxyPass = options.proxy; const socketIO = !options.disableSocket; const content = createNginxConfigContent({ port: options.port, name: deployName, unixSocket: options.unixSocket, proxyPass: proxyPass, socketIO: socketIO, clientMaxBodySize: options.clientMaxBodySize }); await Deno.writeTextFile("/etc/nginx/sites-available/"+deployName,content); await Deno.symlink("/etc/nginx/sites-available/"+deployName,"/etc/nginx/sites-enabled/"+deployName); if(services.map(x=>x.name).includes(deployName)){ const target = services.find(x=>x.name == deployName); if(target){ target.port = options.port; target.unixSocketPath = options.unixSocket; } } else { services.push({ name: deployName, port: options.port, unixSocketPath: options.unixSocket, }); } await saveService(services); if(!await NginxCheck()){ throw new NginxError("nginx config grammar failed"); } if(!await NginxReload()){ throw new NginxError("nginx reload failed"); } } if (import.meta.main) { const cmd = new Command() .name("deployNginx") .version("1.0.2") .description("CLI") .action(()=>{ console.log("sub command required"); }) .command("deploy", "deploy app") .option("-p, --port ","port for app") .option("--unixSocket ","unix socket path") .option("--disableSocket","disable socket io") .option("--clientMaxBodySize ","client max body size: MB Unit",{ default: 20 }) .option("--proxy ","proxy pass for app. you can bind unix socket like \"unix:\"") .arguments("") .action(async (options, name)=>{ try { await deployCommand(name,{ port: options.port, unixSocket: options.unixSocket, clientMaxBodySize: options.clientMaxBodySize, disableSocket: options.disableSocket, proxy: options.proxy }); } catch(e){ if (e instanceof Error){ console.log(e.message); } } }) .command("undeploy","undeploy app") .arguments("") .action(async (_,name)=>{ const services = await loadService(); const i = services.findIndex(x=>x.name == name); if(i < 0){ console.log("not deployed"); Deno.exit(1); } services.splice(i,1); await Deno.remove("/etc/nginx/sites-enabled/"+name); await Deno.remove("/etc/nginx/sites-available/"+name); await saveService(services); if(!await NginxReload()){ console.log("error! nginx reload failed"); Deno.exit(1); } console.log(`success to unload ${name}`); }) .command("list","list deployed service") .action(async ()=>{ const services = await loadService(); if(!Deno.stdout.isTerminal()){ for (const service of services) { printf("%s %s\n",service.name, (service.port ?? service.unixSocketPath ?? "ERROR").toString()); } } else { const maxServiceNameLength = services.map(x=>x.name.length).reduce((x,y)=>Math.max(x,y), 0); const maxPadLength = Math.max(6,maxServiceNameLength); const maxServiceOrNumberLength = services.map(x=> Math.max((x.port ?? 0).toString().length, x.unixSocketPath?.length ?? 0)).reduce((x,y)=>Math.max(x,y), 0); const maxPad2Length = Math.max("PORT OR SOCKET PATH".length, maxServiceOrNumberLength); const prettyPrint = (name: string, port: string) => { printf("%s %s\n",brightGreen(name.padEnd(maxPadLength, " ")), brightMagenta(port.padEnd(maxPad2Length) )); } prettyPrint("NAME","PORT OR SOCKET PATH"); for (const service of services) { prettyPrint(service.name,(service.port ?? service.unixSocketPath ?? "ERROR").toString()); } } }); await cmd.parse(Deno.args); }