229 lines
6.1 KiB
TypeScript
229 lines
6.1 KiB
TypeScript
|
#!/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<DeployedService[]> {
|
||
|
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:number>","port for app",{
|
||
|
required: true
|
||
|
})
|
||
|
.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")
|
||
|
.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,
|
||
|
});
|
||
|
}
|
||
|
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("<value:string>")
|
||
|
.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);
|
||
|
}
|