DeployScript/deploy.ts

289 lines
8.0 KiB
TypeScript
Raw Permalink Normal View History

2024-03-25 23:20:42 +09:00
#!/usr/bin/env -S /root/.deno/bin/deno run --allow-env --allow-read --allow-sys --allow-run --allow-write
2022-11-01 11:52:44 +09:00
2024-03-25 23:20:42 +09:00
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";
2022-11-01 11:52:44 +09:00
import { Command } from "https://deno.land/x/cliffy@v0.25.2/command/mod.ts";
2024-03-25 23:20:42 +09:00
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 {}
2022-11-01 11:52:44 +09:00
interface DeployedService{
name: string,
2024-03-25 23:20:42 +09:00
port?: number,
unixSocketPath?: string,
2022-11-01 11:52:44 +09:00
}
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 {
name: string,
2024-03-25 23:20:42 +09:00
/**
* port number
*/
port?: number,
/**
* unix socket path
*/
unixSocket?: string,
2022-11-01 11:52:44 +09:00
/**
* megabyte unit
*/
clientMaxBodySize?: number,
/**
* proxy pass
* @default "127.0.0.1"
*/
proxyPass?: string,
/**
* socket IO support
* @default true
*/
socketIO?: boolean,
}
2024-03-25 23:20:42 +09:00
function createNginxConfigContent({ port, unixSocket, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) {
2022-11-01 11:52:44 +09:00
clientMaxBodySize ??= 20;
proxyPass ??= "127.0.0.1";
socketIO ??= true;
2024-03-25 23:20:42 +09:00
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.")
}
2022-11-01 11:52:44 +09:00
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;
2024-03-25 23:20:42 +09:00
proxy_pass ${
port ? `http://${proxyPass}:${port}` : `http://unix:${unixSocket}/`
};
2022-11-01 11:52:44 +09:00
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() {
2024-03-25 23:20:42 +09:00
const c = new Deno.Command("nginx",{
args: ["-t"],
2022-11-01 11:52:44 +09:00
});
2024-03-25 23:20:42 +09:00
const status = await c.output();
2022-11-01 11:52:44 +09:00
return status.success && status.code == 0;
}
async function NginxReload() {
2024-03-25 23:20:42 +09:00
const c = new Deno.Command("nginx",{
args: ["-s", "reload"],
2022-11-01 11:52:44 +09:00
});
2024-03-25 23:20:42 +09:00
const status = await c.output();
2022-11-01 11:52:44 +09:00
return status.success && status.code == 0;
}
2024-03-25 23:20:42 +09:00
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");
}
}
2022-11-01 11:52:44 +09:00
if (import.meta.main) {
const cmd = new Command()
.name("deployNginx")
2024-03-25 23:20:42 +09:00
.version("1.0.2")
2022-11-01 11:52:44 +09:00
.description("CLI")
.action(()=>{
console.log("sub command required");
})
.command("deploy", "deploy app")
2024-03-25 23:20:42 +09:00
.option("-p, --port <port:number>","port for app")
.option("--unixSocket <unixSocket:string>","unix socket path")
2022-11-01 11:52:44 +09:00
.option("--disableSocket","disable socket io")
.option("--clientMaxBodySize <clientMaxBodySize:number>","client max body size: MB Unit",{
default: 20
})
2024-03-25 23:20:42 +09:00
.option("--proxy <proxy:string>","proxy pass for app. you can bind unix socket like \"unix:<your unix socket path>\"")
2022-11-01 11:52:44 +09:00
.arguments("<value:string>")
.action(async (options, name)=>{
2024-03-25 23:20:42 +09:00
try {
await deployCommand(name,{
port: options.port,
unixSocket: options.unixSocket,
clientMaxBodySize: options.clientMaxBodySize,
disableSocket: options.disableSocket,
proxy: options.proxy
2022-11-01 11:52:44 +09:00
});
}
2024-03-25 23:20:42 +09:00
catch(e){
if (e instanceof Error){
console.log(e.message);
}
2022-11-01 11:52:44 +09:00
}
})
.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();
2024-03-25 23:20:42 +09:00
if(!Deno.stdout.isTerminal()){
2022-11-01 11:52:44 +09:00
for (const service of services) {
2024-03-25 23:20:42 +09:00
printf("%s %s\n",service.name, (service.port ?? service.unixSocketPath ?? "ERROR").toString());
2022-11-01 11:52:44 +09:00
}
}
else {
2024-03-25 23:20:42 +09:00
const maxServiceNameLength = services.map(x=>x.name.length).reduce((x,y)=>Math.max(x,y), 0);
2022-11-01 11:52:44 +09:00
const maxPadLength = Math.max(6,maxServiceNameLength);
2024-03-25 23:20:42 +09:00
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);
2022-11-01 11:52:44 +09:00
const prettyPrint = (name: string, port: string) => {
2024-03-25 23:20:42 +09:00
printf("%s %s\n",brightGreen(name.padEnd(maxPadLength, " ")),
brightMagenta(port.padEnd(maxPad2Length)
));
2022-11-01 11:52:44 +09:00
}
2024-03-25 23:20:42 +09:00
prettyPrint("NAME","PORT OR SOCKET PATH");
2022-11-01 11:52:44 +09:00
for (const service of services) {
2024-03-25 23:20:42 +09:00
prettyPrint(service.name,(service.port ?? service.unixSocketPath ?? "ERROR").toString());
2022-11-01 11:52:44 +09:00
}
}
});
await cmd.parse(Deno.args);
2024-03-25 23:20:42 +09:00
}