diff --git a/deploy.ts b/deploy.ts index 0e9ddf2..1bbaab6 100755 --- a/deploy.ts +++ b/deploy.ts @@ -1,12 +1,19 @@ -#!/usr/bin/env -S /home/jaeung/.deno/bin/deno run --allow-env --allow-read --unstable --allow-sys --allow-run --allow-write +#!/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.158.0/fmt/printf.ts"; -import {brightGreen, brightMagenta} from "https://deno.land/std@0.160.0/fmt/colors.ts"; +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 + port?: number, + unixSocketPath?: string, } async function saveService(services: DeployedService[]){ @@ -28,8 +35,18 @@ async function loadService(): Promise { } interface NginxConfigOption { - port: number, name: string, + + /** + * port number + */ + port?: number, + + /** + * unix socket path + */ + unixSocket?: string, + /** * megabyte unit */ @@ -46,10 +63,25 @@ interface NginxConfigOption { socketIO?: boolean, } -function createNginxConfigContent({ port, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) { +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; @@ -61,7 +93,9 @@ server { 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_pass ${ + port ? `http://${proxyPass}:${port}` : `http://unix:${unixSocket}/` + }; proxy_redirect off; # client body size @@ -85,99 +119,122 @@ function isRunAsRoot() { } async function NginxCheck() { - const p = Deno.run({ - cmd: ["nginx", "-t"] + const c = new Deno.Command("nginx",{ + args: ["-t"], }); - const status = (await p.status()) + const status = await c.output(); return status.success && status.code == 0; } async function NginxReload() { - const p = Deno.run({ - cmd: ["nginx", "-s","reload"] + const c = new Deno.Command("nginx",{ + args: ["-s", "reload"], }); - const status = (await p.status()) + 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.1") + .version("1.0.2") .description("CLI") .action(()=>{ console.log("sub command required"); }) .command("deploy", "deploy app") - .option("-p, --port ","port for app",{ - required: true - }) + .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") + .option("--proxy ","proxy pass for app. you can bind unix socket like \"unix:\"") .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, + try { + await deployCommand(name,{ + port: options.port, + unixSocket: options.unixSocket, + clientMaxBodySize: options.clientMaxBodySize, + disableSocket: options.disableSocket, + proxy: options.proxy }); } - 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); + catch(e){ + if (e instanceof Error){ + console.log(e.message); + } } }) .command("undeploy","undeploy app") @@ -204,26 +261,28 @@ if (import.meta.main) { .command("list","list deployed service") .action(async ()=>{ const services = await loadService(); - if(!Deno.isatty(Deno.stdout.rid)){ + if(!Deno.stdout.isTerminal()){ for (const service of services) { - printf("%s %s\n",service.name, service.port.toString()); + printf("%s %s\n",service.name, (service.port ?? service.unixSocketPath ?? "ERROR").toString()); } } else { - const maxServiceNameLength = services.length == 0 ? 0 : services.map(x=>x.name.length).reduce((x,y)=>Math.max(x,y)); + 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) + " ".repeat(maxPadLength - name.length), - brightMagenta(port.padStart(5 - port.length))); + printf("%s %s\n",brightGreen(name.padEnd(maxPadLength, " ")), + brightMagenta(port.padEnd(maxPad2Length) + )); } - prettyPrint("NAME","PORT"); + prettyPrint("NAME","PORT OR SOCKET PATH"); for (const service of services) { - prettyPrint(service.name,service.port.toString()); + prettyPrint(service.name,(service.port ?? service.unixSocketPath ?? "ERROR").toString()); } } }); await cmd.parse(Deno.args); -} \ No newline at end of file +}