feat: add unix socket
This commit is contained in:
		
							parent
							
								
									b6bf8e2367
								
							
						
					
					
						commit
						5f3f3d80fd
					
				
					 1 changed files with 142 additions and 83 deletions
				
			
		
							
								
								
									
										225
									
								
								deploy.ts
									
										
									
									
									
								
							
							
						
						
									
										225
									
								
								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<DeployedService[]> {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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<void>{
 | 
			
		||||
  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:number>","port for app",{
 | 
			
		||||
      required: true
 | 
			
		||||
    })
 | 
			
		||||
    .option("-p, --port <port:number>","port for app")
 | 
			
		||||
    .option("--unixSocket <unixSocket:string>","unix socket path")
 | 
			
		||||
    .option("--disableSocket","disable socket io")
 | 
			
		||||
    .option("--clientMaxBodySize <clientMaxBodySize:number>","client max body size: MB Unit",{
 | 
			
		||||
      default: 20
 | 
			
		||||
    })
 | 
			
		||||
    .option("--proxy <proxy:string>","proxy pass for app")
 | 
			
		||||
    .option("--proxy <proxy:string>","proxy pass for app. you can bind unix socket like \"unix:<your unix socket path>\"")
 | 
			
		||||
    .arguments("<value:string>")
 | 
			
		||||
    .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);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue