Compare commits

...

2 Commits

Author SHA1 Message Date
5f3f3d80fd feat: add unix socket 2024-03-25 23:20:42 +09:00
b6bf8e2367 add renew dns 2024-02-07 22:42:33 +09:00
3 changed files with 207 additions and 84 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.vscode .vscode
services.json services.json
.vscode/**/* .vscode/**/*
.env

225
deploy.ts
View File

@ -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 { printf } from "https://deno.land/std@0.170.0/fmt/printf.ts";
import {brightGreen, brightMagenta} from "https://deno.land/std@0.160.0/fmt/colors.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 { 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{ interface DeployedService{
name: string, name: string,
port: number port?: number,
unixSocketPath?: string,
} }
async function saveService(services: DeployedService[]){ async function saveService(services: DeployedService[]){
@ -28,8 +35,18 @@ async function loadService(): Promise<DeployedService[]> {
} }
interface NginxConfigOption { interface NginxConfigOption {
port: number,
name: string, name: string,
/**
* port number
*/
port?: number,
/**
* unix socket path
*/
unixSocket?: string,
/** /**
* megabyte unit * megabyte unit
*/ */
@ -46,10 +63,25 @@ interface NginxConfigOption {
socketIO?: boolean, socketIO?: boolean,
} }
function createNginxConfigContent({ port, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) { function createNginxConfigContent({ port, unixSocket, name, clientMaxBodySize, proxyPass, socketIO }: NginxConfigOption) {
clientMaxBodySize ??= 20; clientMaxBodySize ??= 20;
proxyPass ??= "127.0.0.1"; proxyPass ??= "127.0.0.1";
socketIO ??= true; 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. const content = `# it created by deploy script.
server { server {
server_name ${name}.prelude.duckdns.org; 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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true; proxy_set_header X-NginX-Proxy true;
proxy_pass http://${proxyPass}:${port}; proxy_pass ${
port ? `http://${proxyPass}:${port}` : `http://unix:${unixSocket}/`
};
proxy_redirect off; proxy_redirect off;
# client body size # client body size
@ -85,99 +119,122 @@ function isRunAsRoot() {
} }
async function NginxCheck() { async function NginxCheck() {
const p = Deno.run({ const c = new Deno.Command("nginx",{
cmd: ["nginx", "-t"] args: ["-t"],
}); });
const status = (await p.status()) const status = await c.output();
return status.success && status.code == 0; return status.success && status.code == 0;
} }
async function NginxReload() { async function NginxReload() {
const p = Deno.run({ const c = new Deno.Command("nginx",{
cmd: ["nginx", "-s","reload"] args: ["-s", "reload"],
}); });
const status = (await p.status()) const status = await c.output();
return status.success && status.code == 0; 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) { if (import.meta.main) {
const cmd = new Command() const cmd = new Command()
.name("deployNginx") .name("deployNginx")
.version("1.0.1") .version("1.0.2")
.description("CLI") .description("CLI")
.action(()=>{ .action(()=>{
console.log("sub command required"); console.log("sub command required");
}) })
.command("deploy", "deploy app") .command("deploy", "deploy app")
.option("-p, --port <port:number>","port for app",{ .option("-p, --port <port:number>","port for app")
required: true .option("--unixSocket <unixSocket:string>","unix socket path")
})
.option("--disableSocket","disable socket io") .option("--disableSocket","disable socket io")
.option("--clientMaxBodySize <clientMaxBodySize:number>","client max body size: MB Unit",{ .option("--clientMaxBodySize <clientMaxBodySize:number>","client max body size: MB Unit",{
default: 20 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>") .arguments("<value:string>")
.action(async (options, name)=>{ .action(async (options, name)=>{
const deployPort = options.port; try {
const deployName = name; await deployCommand(name,{
port: options.port,
if(deployName.includes("/")){ unixSocket: options.unixSocket,
console.log("name invalid"); clientMaxBodySize: options.clientMaxBodySize,
return; disableSocket: options.disableSocket,
} proxy: options.proxy
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); catch(e){
if (e instanceof Error){
if(!await NginxCheck()){ console.log(e.message);
console.log("nginx config grammar failed"); }
Deno.exit(1);
}
if(!await NginxReload()){
console.log("nginx reload failed");
Deno.exit(1);
} }
}) })
.command("undeploy","undeploy app") .command("undeploy","undeploy app")
@ -204,26 +261,28 @@ if (import.meta.main) {
.command("list","list deployed service") .command("list","list deployed service")
.action(async ()=>{ .action(async ()=>{
const services = await loadService(); const services = await loadService();
if(!Deno.isatty(Deno.stdout.rid)){ if(!Deno.stdout.isTerminal()){
for (const service of services) { 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 { 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 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) => { const prettyPrint = (name: string, port: string) => {
printf("%s %s\n",brightGreen(name) + " ".repeat(maxPadLength - name.length), printf("%s %s\n",brightGreen(name.padEnd(maxPadLength, " ")),
brightMagenta(port.padStart(5 - port.length))); brightMagenta(port.padEnd(maxPad2Length)
));
} }
prettyPrint("NAME","PORT"); prettyPrint("NAME","PORT OR SOCKET PATH");
for (const service of services) { 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); await cmd.parse(Deno.args);
} }

64
renew_dns.ts Executable file
View File

@ -0,0 +1,64 @@
#!/usr/bin/env -S /root/.deno/bin/deno run --allow-env --allow-read --unstable --allow-sys --allow-run --allow-write
import {load} from "https://deno.land/std@0.214.0/dotenv/mod.ts";
import { Command } from "https://deno.land/x/cliffy@v0.25.7/mod.ts";
async function changeTextRecord(domain: string, token:string, text: string){
const url = `https://www.duckdns.org/update?domains=${domain}&token=${token}&txt=${text}`
const res = await fetch(url);
const c = await res.text()
return c === "OK"
}
const env = await load();
const domain = env.DUCKDNS_DOMAIN;
const token = env.DUCKDNS_TOKEN;
/*
sudo certbot certonly --manual --preferred-challenges dns --renew-by-default -d "*.pages.prelude.duckdns.org"
*/
if(import.meta.main){
if (!domain || !token){
console.log("Env not found!");
Deno.exit(1);
}
//const cmd = new Deno.Command("certbot",{
// args:[
// "certonly",
// "--manual",
// "--preferred-challenges",
// "dns",
// "--renew-by-default",
// "-d", "*.pages.prelude.duckdns.org"
// ],
// stdin: "piped",
// stdout: "piped",
//});
//
//const proc = cmd.spawn();
//await proc.stdout.pipeTo(Deno.stdout.writable);
await new Command()
.name("renew_dns")
.description("Duckdns TXT Record renewer")
.version("v1.0.0")
.option("-q, --quiet", "disable output.")
.arguments("<text>")
.action(async ({quiet},text)=>{
const s = await changeTextRecord(domain, token, text);
if (s) {
if (!quiet){
console.log("success!");
}
Deno.exit(0);
}
else {
if(!quiet){
console.log("failed!");
}
Deno.exit(1);
}
})
.parse(Deno.args);
}