#!/usr/bin/env -S /home/jaeung/.deno/bin/deno run --allow-env --allow-read --unstable --allow-sys --allow-run --allow-write import { printf } from "https://deno.land/std@0.158.0/fmt/printf.ts"; import {brightGreen, brightMagenta} from "https://deno.land/std@0.160.0/fmt/colors.ts"; import { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts"; interface DeployedService{ name: string, port: number } 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 { port: number, name: string, /** * megabyte unit */ clientMaxBodySize?: number, /** * proxy pass * @default "127.0.0.1" */ proxyPass?: string, /** * socket IO support * @default true */ socketIO?: boolean, } function createNginxConfigContent({ port, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) { clientMaxBodySize ??= 20; proxyPass ??= "127.0.0.1"; socketIO ??= true; 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 http://${proxyPass}:${port}; 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 p = Deno.run({ cmd: ["nginx", "-t"] }); const status = (await p.status()) return status.success && status.code == 0; } async function NginxReload() { const p = Deno.run({ cmd: ["nginx", "-s","reload"] }); const status = (await p.status()) return status.success && status.code == 0; } if (import.meta.main) { const cmd = new Command() .name("deployNginx") .version("1.0.1") .description("CLI") .action(()=>{ console.log("sub command required"); }) .command("deploy", "deploy app") .option("-p, --port ","port for app",{ required: true }) .option("--disableSocket","disable socket io") .option("--clientMaxBodySize ","client max body size: MB Unit",{ default: 20 }) .option("--proxy ","proxy pass for app") .arguments("") .action(async (options, name)=>{ const deployPort = options.port; const deployName = name; if(deployName.includes("/")){ console.log("name invalid"); return; } if(!isRunAsRoot()){ console.log("Warning! It's not executed as root"); } else{ console.log("run as root"); } const services = await loadService(); const dir = [...Deno.readDirSync("/etc/nginx/sites-available")] if (dir.map(x => x.name).includes(deployName)) { console.log("duplicate!") Deno.exit(1); } const proxyPass = options.proxy; const socketIO = !options.disableSocket; const content = createNginxConfigContent({ port: deployPort, name: deployName, 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 = deployPort; } } else { services.push({ name: deployName, port: deployPort, }); } await saveService(services); if(!await NginxCheck()){ console.log("nginx config grammar failed"); Deno.exit(1); } if(!await NginxReload()){ console.log("nginx reload failed"); Deno.exit(1); } }) .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.isatty(Deno.stdout.rid)){ for (const service of services) { printf("%s %s\n",service.name, service.port.toString()); } } else { const maxServiceNameLength = services.length == 0 ? 0 : services.map(x=>x.name.length).reduce((x,y)=>Math.max(x,y)); const maxPadLength = Math.max(6,maxServiceNameLength); const prettyPrint = (name: string, port: string) => { printf("%s %s\n",brightGreen(name) + " ".repeat(maxPadLength - name.length), brightMagenta(port.padStart(5 - port.length))); } prettyPrint("NAME","PORT"); for (const service of services) { prettyPrint(service.name,service.port.toString()); } } }); await cmd.parse(Deno.args); }