Compare commits
20 Commits
dd36e2ab1a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f0631aa35 | |||
| 5eef06bd42 | |||
| 1f037113bf | |||
| ee8b7e5660 | |||
| 5ccbc592b7 | |||
| b006d29f53 | |||
| a11af366ef | |||
| 2240ac30c6 | |||
| 34f20e02ae | |||
| 11a9a4a929 | |||
| 2481d2b616 | |||
| 35612d2f18 | |||
| 99e4a64b5a | |||
| 498eb05514 | |||
| 8a5cc34bb4 | |||
| 006a22cd7b | |||
| 2cfc9cd7fa | |||
| 2125239d42 | |||
| 70e5e5eddf | |||
| f22affc05c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/letsencrypt
|
/letsencrypt
|
||||||
/certs
|
/certs
|
||||||
/dist
|
/dist
|
||||||
|
/logs
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +1,7 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
@@ -3,12 +3,13 @@ FROM golang:alpine as roadsign-server
|
|||||||
|
|
||||||
WORKDIR /source
|
WORKDIR /source
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/server/main.go
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/main.go
|
||||||
|
|
||||||
# Runtime
|
# Runtime
|
||||||
FROM golang:alpine
|
FROM golang:alpine
|
||||||
|
|
||||||
RUN apk add zip
|
RUN apk add zip
|
||||||
|
RUN apk add nodejs npm
|
||||||
|
|
||||||
COPY --from=roadsign-server /dist /roadsign/server
|
COPY --from=roadsign-server /dist /roadsign/server
|
||||||
|
|
||||||
|
|||||||
13
cli/.roadsignrc
Normal file
13
cli/.roadsignrc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"sync": {
|
||||||
|
"region": "static-files",
|
||||||
|
"configPath": "test/static-files.toml"
|
||||||
|
},
|
||||||
|
"deployments": [
|
||||||
|
{
|
||||||
|
"path": "test/static-files",
|
||||||
|
"region": "static-files",
|
||||||
|
"site": "static-files-des"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
cli/bun.lockb
BIN
cli/bun.lockb
Binary file not shown.
8
cli/index.ts
Normal file → Executable file
8
cli/index.ts
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
import { Cli } from "clipanion"
|
import { Builtins, Cli } from "clipanion"
|
||||||
import figlet from "figlet"
|
import figlet from "figlet"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ import { StatusCommand } from "./src/cmd/status.ts"
|
|||||||
import { InfoCommand } from "./src/cmd/info.ts"
|
import { InfoCommand } from "./src/cmd/info.ts"
|
||||||
import { ProcessCommand } from "./src/cmd/process-info.ts"
|
import { ProcessCommand } from "./src/cmd/process-info.ts"
|
||||||
import { DeployCommand } from "./src/cmd/deploy.ts"
|
import { DeployCommand } from "./src/cmd/deploy.ts"
|
||||||
|
import { SyncCommand } from "./src/cmd/sync.ts"
|
||||||
|
import { ReloadCommand } from "./src/cmd/reload.ts"
|
||||||
|
|
||||||
const [node, app, ...args] = process.argv
|
const [node, app, ...args] = process.argv
|
||||||
|
|
||||||
@@ -26,6 +28,8 @@ const cli = new Cli({
|
|||||||
binaryVersion: `1.0.0`
|
binaryVersion: `1.0.0`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cli.register(Builtins.VersionCommand)
|
||||||
|
cli.register(Builtins.HelpCommand)
|
||||||
cli.register(LoginCommand)
|
cli.register(LoginCommand)
|
||||||
cli.register(LogoutCommand)
|
cli.register(LogoutCommand)
|
||||||
cli.register(ListServerCommand)
|
cli.register(ListServerCommand)
|
||||||
@@ -33,4 +37,6 @@ cli.register(StatusCommand)
|
|||||||
cli.register(InfoCommand)
|
cli.register(InfoCommand)
|
||||||
cli.register(ProcessCommand)
|
cli.register(ProcessCommand)
|
||||||
cli.register(DeployCommand)
|
cli.register(DeployCommand)
|
||||||
|
cli.register(SyncCommand)
|
||||||
|
cli.register(ReloadCommand)
|
||||||
cli.runExit(args)
|
cli.runExit(args)
|
||||||
@@ -1,16 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "roadsign-cli",
|
"name": "roadsign-cli",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"repository": "https://github.com/solsynth/roadsign",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf dist && rollup -c rollup.config.js"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rdcli": "./dist/index.cjs"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.0",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||||
|
"@rollup/plugin-typescript": "^12.1.0",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/figlet": "^1.5.8"
|
"@types/cli-progress": "^3.11.6",
|
||||||
|
"@types/figlet": "^1.5.8",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"rollup": "^4.24.0",
|
||||||
|
"rollup-plugin-typescript2": "^0.36.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"cli-table3": "^0.6.5",
|
"cli-table3": "^0.6.5",
|
||||||
"clipanion": "^4.0.0-rc.4",
|
"clipanion": "^4.0.0-rc.4",
|
||||||
"figlet": "^1.7.0",
|
"figlet": "^1.7.0",
|
||||||
|
|||||||
22
cli/rollup.config.js
Normal file
22
cli/rollup.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
|
import typescript from "@rollup/plugin-typescript"
|
||||||
|
import json from "@rollup/plugin-json"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "index.ts",
|
||||||
|
output: {
|
||||||
|
banner: "#!/usr/bin/env node",
|
||||||
|
file: "dist/index.cjs",
|
||||||
|
format: "cjs",
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
typescript({
|
||||||
|
tsconfig: "./tsconfig.json"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import * as fs from "node:fs"
|
|||||||
import * as child_process from "node:child_process"
|
import * as child_process from "node:child_process"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import { createAuthHeader } from "../utils/auth.ts"
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import { RsLocalConfig, type RsLocalConfigDeploymentPostActionData } from "../utils/config-local.ts"
|
||||||
|
import * as os from "node:os"
|
||||||
|
|
||||||
export class DeployCommand extends Command {
|
export class DeployCommand extends Command {
|
||||||
static paths = [[`deploy`]]
|
static paths = [[`deploy`]]
|
||||||
@@ -13,54 +15,63 @@ export class DeployCommand extends Command {
|
|||||||
category: `Building`,
|
category: `Building`,
|
||||||
description: `Deploying App / Static Site onto RoadSign`,
|
description: `Deploying App / Static Site onto RoadSign`,
|
||||||
details: `Deploying an application or hosting a static site via RoadSign, you need preconfigured the RoadSign, or sync the configurations via sync command.`,
|
details: `Deploying an application or hosting a static site via RoadSign, you need preconfigured the RoadSign, or sync the configurations via sync command.`,
|
||||||
examples: [["Deploying to RoadSign", `deploy <server> <site> <slug> <file / directory>`]]
|
examples: [
|
||||||
|
["Deploying to RoadSign", `deploy <server> <region> <site> <file / directory>`],
|
||||||
|
["Deploying to RoadSign with .roadsignrc file", `deploy <server>`]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
server = Option.String({ required: true })
|
server = Option.String({ required: true })
|
||||||
site = Option.String({ required: true })
|
region = Option.String({ required: false })
|
||||||
upstream = Option.String({ required: true })
|
site = Option.String({ required: false })
|
||||||
input = Option.String({ required: true })
|
input = Option.String({ required: false })
|
||||||
|
|
||||||
async execute() {
|
async deploy(serverLabel: string, region: string, site: string, input: string, postDeploy: RsLocalConfigDeploymentPostActionData | null = null) {
|
||||||
const config = await RsConfig.getInstance()
|
const cfg = await RsConfig.getInstance()
|
||||||
|
const server = cfg.config.servers.find(item => item.label === serverLabel)
|
||||||
const server = config.config.servers.find(item => item.label === this.server)
|
|
||||||
if (server == null) {
|
if (server == null) {
|
||||||
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(this.input)) {
|
if (!fs.existsSync(input)) {
|
||||||
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`))
|
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let isDirectory = false
|
let isDirectory = false
|
||||||
if (fs.statSync(this.input).isDirectory()) {
|
if (fs.statSync(input).isDirectory()) {
|
||||||
if (this.input.endsWith("/")) {
|
|
||||||
this.input = this.input.slice(0, -1)
|
|
||||||
}
|
|
||||||
this.input += "/*"
|
|
||||||
|
|
||||||
const compressPrefStart = performance.now()
|
const compressPrefStart = performance.now()
|
||||||
const compressSpinner = ora(`Compressing ${chalk.bold(this.input)}...`).start()
|
const compressSpinner = ora(`Compressing ${chalk.bold(input)}...`).start()
|
||||||
const destName = `${Date.now()}-roadsign-archive.zip`
|
const destName = path.join(os.tmpdir(), `${Date.now()}-roadsign-archive.zip`)
|
||||||
child_process.execSync(`zip -rj ${destName} ${this.input}`)
|
child_process.execSync(`cd ${input} && zip -r ${destName} .`)
|
||||||
const compressPrefTook = performance.now() - compressPrefStart
|
const compressPrefTook = performance.now() - compressPrefStart
|
||||||
compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`)
|
compressSpinner.succeed(`Compressing completed in ${(compressPrefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
this.input = destName
|
input = destName
|
||||||
isDirectory = true
|
isDirectory = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const destBreadcrumb = [this.site, this.upstream].join(" ➜ ")
|
const destBreadcrumb = [region, site].join(" ➜ ")
|
||||||
const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start()
|
const spinner = ora(`Deploying ${chalk.bold(destBreadcrumb)} to ${chalk.bold(this.server)}...`).start()
|
||||||
|
|
||||||
const prefStart = performance.now()
|
const prefStart = performance.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = new FormData()
|
const payload = new FormData()
|
||||||
payload.set("attachments", await fs.openAsBlob(this.input), isDirectory ? "dist.zip" : path.basename(this.input))
|
payload.set("attachments", await fs.openAsBlob(input), isDirectory ? "dist.zip" : path.basename(input))
|
||||||
const res = await fetch(`${server.url}/webhooks/publish/${this.site}/${this.upstream}?mimetype=application/zip`, {
|
|
||||||
|
if(postDeploy) {
|
||||||
|
if(postDeploy.command) {
|
||||||
|
payload.set("post-deploy-script", postDeploy.command)
|
||||||
|
} else if(postDeploy.scriptPath) {
|
||||||
|
payload.set("post-deploy-script", fs.readFileSync(postDeploy.scriptPath, "utf8"))
|
||||||
|
} else {
|
||||||
|
this.context.stdout.write(chalk.yellow(`Configured post deploy action but no script provided, skip performing post deploy action...\n`))
|
||||||
|
}
|
||||||
|
payload.set("post-deploy-environment", postDeploy.environment?.join("\n") ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${server.url}/webhooks/publish/${region}/${site}?mimetype=application/zip`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: payload,
|
body: payload,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -76,10 +87,37 @@ export class DeployCommand extends Command {
|
|||||||
this.context.stdout.write(`Failed to deploy to remote: ${e}\n`)
|
this.context.stdout.write(`Failed to deploy to remote: ${e}\n`)
|
||||||
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
||||||
} finally {
|
} finally {
|
||||||
if (isDirectory && this.input.endsWith(".zip")) {
|
if (isDirectory && input.endsWith(".zip")) {
|
||||||
fs.unlinkSync(this.input)
|
fs.unlinkSync(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
if (this.region && this.site && this.input) {
|
||||||
|
await this.deploy(this.server, this.region, this.site, this.input)
|
||||||
|
} else {
|
||||||
|
let localCfg: RsLocalConfig
|
||||||
|
try {
|
||||||
|
localCfg = await RsLocalConfig.getInstance()
|
||||||
|
} catch (e) {
|
||||||
|
this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localCfg.config.deployments) {
|
||||||
|
this.context.stdout.write(chalk.red(`No deployments found in .roadsignrc, exiting...\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0
|
||||||
|
for (const deployment of localCfg.config.deployments ?? []) {
|
||||||
|
this.context.stdout.write(chalk.cyan(`Deploying ${idx + 1} out of ${localCfg.config.deployments.length} deployments...\n`))
|
||||||
|
await this.deploy(this.server, deployment.region, deployment.site, deployment.path, deployment.postDeploy)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.stdout.write(chalk.green(`All deployments has been deployed!\n`))
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Command, Option, type Usage } from "clipanion"
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
import { RsConfig } from "../utils/config.ts"
|
import { RsConfig, type RsConfigServerData } from "../utils/config.ts"
|
||||||
import { createAuthHeader } from "../utils/auth.ts"
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import ora from "ora"
|
import ora from "ora"
|
||||||
@@ -37,6 +37,77 @@ export class InfoCommand extends Command {
|
|||||||
return uptimeParts.join(", ")
|
return uptimeParts.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchOverview(server: RsConfigServerData) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/stats`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await res.json()
|
||||||
|
this.context.stdout.write('\n')
|
||||||
|
this.context.stdout.write(`\nServer stats of ${chalk.bold(this.label)}\n`)
|
||||||
|
this.context.stdout.write(` • Uptime: ${chalk.bold(InfoCommand.formatUptime(data["uptime"]))}\n`)
|
||||||
|
this.context.stdout.write(` • Traffic since last startup: ${chalk.bold(data["traffic"]["total"])}\n`)
|
||||||
|
this.context.stdout.write(` • Unique clients since last startup: ${chalk.bold(data["traffic"]["unique_client"])}\n`)
|
||||||
|
this.context.stdout.write(`\nServer info of ${chalk.bold(this.label)}\n`)
|
||||||
|
this.context.stdout.write(` • Warden Applications: ${chalk.bold(data["applications"])}\n`)
|
||||||
|
this.context.stdout.write(` • Destinations: ${chalk.bold(data["destinations"])}\n`)
|
||||||
|
this.context.stdout.write(` • Locations: ${chalk.bold(data["locations"])}\n`)
|
||||||
|
this.context.stdout.write(` • Regions: ${chalk.bold(data["regions"])}\n`)
|
||||||
|
this.context.stdout.write('\n')
|
||||||
|
} catch (e) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchTrace(server: RsConfigServerData) {
|
||||||
|
const res = await fetch(`${server.url}/cgi/traces`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await res.json()
|
||||||
|
for (const trace of data) {
|
||||||
|
const ts = new Date(trace["timestamp"]).toLocaleString()
|
||||||
|
const path = [trace["region"], trace["location"], trace["destination"]].join(" ➜ ")
|
||||||
|
const uri = trace["uri"].split("?").length == 1 ? trace["uri"] : trace["uri"].split("?")[0] + ` ${chalk.grey(`w/ query parameters`)}`
|
||||||
|
this.context.stdout.write(`${chalk.bgGrey(`[${ts}]`)} ${chalk.bold(path)} ${chalk.cyan(trace["ip_address"])} ${uri}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchRegions(server: RsConfigServerData) {
|
||||||
|
const res = await fetch(`${server.url}/cgi/regions`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await res.json()
|
||||||
|
this.context.stdout.write("\n\n")
|
||||||
|
for (const region of data) {
|
||||||
|
this.context.stdout.write(` • ${chalk.bgGrey('region#')}${chalk.bold(region.id)} ${chalk.gray(`(${region.locations.length} locations)`)}\n`)
|
||||||
|
for (const location of region.locations) {
|
||||||
|
this.context.stdout.write(` • ${chalk.bgGrey('location#')} ${chalk.bold(location.id)} ${chalk.gray(`(${location.destinations.length} destinations)`)}\n`)
|
||||||
|
for (const destination of location.destinations) {
|
||||||
|
this.context.stdout.write(` • ${chalk.bgGrey('destination#')}${chalk.bold(destination.id)}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.context.stdout.write("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
const config = await RsConfig.getInstance()
|
const config = await RsConfig.getInstance()
|
||||||
|
|
||||||
@@ -56,55 +127,21 @@ export class InfoCommand extends Command {
|
|||||||
switch (this.area) {
|
switch (this.area) {
|
||||||
case "overview":
|
case "overview":
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${server.url}/cgi/stats`, {
|
await this.fetchOverview(server)
|
||||||
headers: {
|
|
||||||
Authorization: createAuthHeader(server.credential)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(await res.text())
|
|
||||||
}
|
|
||||||
const prefTook = performance.now() - prefStart
|
const prefTook = performance.now() - prefStart
|
||||||
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
this.context.stdout.write(`\nServer stats of ${chalk.bold(this.label)}\n`)
|
|
||||||
this.context.stdout.write(`Uptime: ${chalk.bold(InfoCommand.formatUptime(data["uptime"]))}\n`)
|
|
||||||
this.context.stdout.write(`Traffic since last startup: ${chalk.bold(data["traffic"]["total"])}\n`)
|
|
||||||
this.context.stdout.write(`Unique clients since last startup: ${chalk.bold(data["traffic"]["unique_client"])}\n`)
|
|
||||||
this.context.stdout.write(`\nServer info of ${chalk.bold(this.label)}\n`)
|
|
||||||
this.context.stdout.write(`Warden Applications: ${chalk.bold(data["applications"])}\n`)
|
|
||||||
this.context.stdout.write(`Destinations: ${chalk.bold(data["destinations"])}\n`)
|
|
||||||
this.context.stdout.write(`Locations: ${chalk.bold(data["locations"])}\n`)
|
|
||||||
this.context.stdout.write(`Regions: ${chalk.bold(data["regions"])}\n`)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "trace":
|
case "trace":
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${server.url}/cgi/traces`, {
|
await this.fetchTrace(server)
|
||||||
headers: {
|
|
||||||
Authorization: createAuthHeader(server.credential)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error(await res.text())
|
|
||||||
}
|
|
||||||
const prefTook = performance.now() - prefStart
|
const prefTook = performance.now() - prefStart
|
||||||
if (!this.loop) {
|
if (!this.loop) {
|
||||||
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
for (const trace of data) {
|
|
||||||
const ts = new Date(trace["timestamp"]).toLocaleString()
|
|
||||||
const path = [trace["region"], trace["location"], trace["destination"]].join(" ➜ ")
|
|
||||||
const uri = trace["uri"].split("?").length == 1 ? trace["uri"] : trace["uri"].split("?")[0] + ` ${chalk.grey(`w/ query parameters`)}`
|
|
||||||
this.context.stdout.write(`${chalk.bgGrey(`[${ts}]`)} ${chalk.bold(path)} ${chalk.cyan(trace["ip_address"])} ${uri}\n`)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
return
|
return
|
||||||
@@ -113,12 +150,22 @@ export class InfoCommand extends Command {
|
|||||||
if (!this.loop) {
|
if (!this.loop) {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
spinner.text = 'Updating...'
|
spinner.text = "Updating..."
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||||
this.context.stdout.write('\x1Bc')
|
this.context.stdout.write("\x1Bc")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "regions":
|
||||||
|
try {
|
||||||
|
await this.fetchRegions(server)
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
spinner.succeed(`Fetching completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
} catch (e) {
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.label)} is not running! 😢`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
spinner.fail(chalk.red(`Info area was not exists ${chalk.bold(this.area)}...`))
|
spinner.fail(chalk.red(`Info area was not exists ${chalk.bold(this.area)}...`))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class LoginCommand extends Command {
|
|||||||
if (pingRes.status !== 200) {
|
if (pingRes.status !== 200) {
|
||||||
throw new Error(await pingRes.text())
|
throw new Error(await pingRes.text())
|
||||||
} else {
|
} else {
|
||||||
const info = await pingRes.json()
|
const info: any = await pingRes.json()
|
||||||
spinner.succeed(`Connected to ${this.host}, remote version ${info["version"]}`)
|
spinner.succeed(`Connected to ${this.host}, remote version ${info["version"]}`)
|
||||||
|
|
||||||
config.config.servers.push({
|
config.config.servers.push({
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class ProcessCommand extends Command {
|
|||||||
|
|
||||||
const statusMapping = ["Created", "Starting", "Started", "Exited", "Failed"]
|
const statusMapping = ["Created", "Starting", "Started", "Exited", "Failed"]
|
||||||
|
|
||||||
const data = await res.json()
|
const data: any = await res.json()
|
||||||
for (const app of data) {
|
for (const app of data) {
|
||||||
table.push([app["id"], statusMapping[app["status"]], app["command"].join(" ")])
|
table.push([app["id"], statusMapping[app["status"]], app["command"].join(" ")])
|
||||||
}
|
}
|
||||||
|
|||||||
53
cli/src/cmd/reload.ts
Normal file
53
cli/src/cmd/reload.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { RsConfig } from "../utils/config.ts"
|
||||||
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import ora from "ora"
|
||||||
|
import * as fs from "node:fs"
|
||||||
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import { RsLocalConfig } from "../utils/config-local.ts"
|
||||||
|
|
||||||
|
export class ReloadCommand extends Command {
|
||||||
|
static paths = [[`reload`]]
|
||||||
|
static usage: Usage = {
|
||||||
|
category: `Building`,
|
||||||
|
description: `Reload configuration on RoadSign`,
|
||||||
|
details: `Reload configuration on remote RoadSign to make changes applied.`,
|
||||||
|
examples: [
|
||||||
|
["Reload an connected server", `reload <server>`],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
server = Option.String({ required: true })
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
const cfg = await RsConfig.getInstance()
|
||||||
|
const server = cfg.config.servers.find(item => item.label === this.server)
|
||||||
|
if (server == null) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora(`Reloading server ${chalk.bold(this.server)}...`).start()
|
||||||
|
|
||||||
|
const prefStart = performance.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/cgi/reload`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
spinner.succeed(`Reloading completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
} catch (e) {
|
||||||
|
this.context.stdout.write(`Failed to reload remote: ${e}\n`)
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
cli/src/cmd/sync.ts
Normal file
87
cli/src/cmd/sync.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { RsConfig } from "../utils/config.ts"
|
||||||
|
import { Command, Option, type Usage } from "clipanion"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import ora from "ora"
|
||||||
|
import * as fs from "node:fs"
|
||||||
|
import { createAuthHeader } from "../utils/auth.ts"
|
||||||
|
import { RsLocalConfig } from "../utils/config-local.ts"
|
||||||
|
|
||||||
|
export class SyncCommand extends Command {
|
||||||
|
static paths = [[`sync`]]
|
||||||
|
static usage: Usage = {
|
||||||
|
category: `Building`,
|
||||||
|
description: `Sync configuration to RoadSign over Sideload`,
|
||||||
|
details: `Update remote RoadSign configuration with local ones.`,
|
||||||
|
examples: [
|
||||||
|
["Sync to RoadSign", `sync <server> <region> <file>`],
|
||||||
|
["Sync to RoadSign with .roadsignrc file", `sync <server>`]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
server = Option.String({ required: true })
|
||||||
|
region = Option.String({ required: false })
|
||||||
|
input = Option.String({ required: false })
|
||||||
|
|
||||||
|
async sync(serverLabel: string, region: string, input: string) {
|
||||||
|
const cfg = await RsConfig.getInstance()
|
||||||
|
const server = cfg.config.servers.find(item => item.label === serverLabel)
|
||||||
|
if (server == null) {
|
||||||
|
this.context.stdout.write(chalk.red(`Server with label ${chalk.bold(this.server)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(input)) {
|
||||||
|
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} was not found.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!fs.statSync(input).isFile()) {
|
||||||
|
this.context.stdout.write(chalk.red(`Input file ${chalk.bold(this.input)} is not a file.\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora(`Syncing ${chalk.bold(region)} to ${chalk.bold(this.server)}...`).start()
|
||||||
|
|
||||||
|
const prefStart = performance.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${server.url}/webhooks/sync/${region}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: fs.readFileSync(input, "utf8"),
|
||||||
|
headers: {
|
||||||
|
Authorization: createAuthHeader(server.credential)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error(await res.text())
|
||||||
|
}
|
||||||
|
const prefTook = performance.now() - prefStart
|
||||||
|
spinner.succeed(`Syncing completed in ${(prefTook / 1000).toFixed(2)}s 🎉`)
|
||||||
|
} catch (e) {
|
||||||
|
this.context.stdout.write(`Failed to sync to remote: ${e}\n`)
|
||||||
|
spinner.fail(`Server with label ${chalk.bold(this.server)} is not running! 😢`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
if (this.region && this.input) {
|
||||||
|
await this.sync(this.server, this.region, this.input)
|
||||||
|
} else {
|
||||||
|
let localCfg: RsLocalConfig
|
||||||
|
try {
|
||||||
|
localCfg = await RsLocalConfig.getInstance()
|
||||||
|
} catch (e) {
|
||||||
|
this.context.stdout.write(chalk.red(`Unable to load .roadsignrc: ${e}\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localCfg.config.sync) {
|
||||||
|
this.context.stdout.write(chalk.red(`No sync configuration found in .roadsignrc, exiting...\n`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sync(this.server, localCfg.config.sync.region, localCfg.config.sync.configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
cli/src/utils/config-local.ts
Normal file
67
cli/src/utils/config-local.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import * as path from "node:path"
|
||||||
|
import * as fs from "node:fs"
|
||||||
|
|
||||||
|
interface RsLocalConfigData {
|
||||||
|
sync?: RsLocalConfigSyncData
|
||||||
|
deployments?: RsLocalConfigDeploymentData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RsLocalConfigSyncData {
|
||||||
|
configPath: string
|
||||||
|
region: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RsLocalConfigDeploymentData {
|
||||||
|
path: string
|
||||||
|
region: string
|
||||||
|
site: string
|
||||||
|
postDeploy?: RsLocalConfigDeploymentPostActionData
|
||||||
|
autoBuild?: RsLocalConfigDeploymentAutoBuildData
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RsLocalConfigDeploymentAutoBuildData {
|
||||||
|
command: string
|
||||||
|
environment?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RsLocalConfigDeploymentPostActionData {
|
||||||
|
command?: string
|
||||||
|
scriptPath?: string
|
||||||
|
environment?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class RsLocalConfig {
|
||||||
|
private static instance: RsLocalConfig
|
||||||
|
|
||||||
|
public config: RsLocalConfigData = {}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getInstance(): Promise<RsLocalConfig> {
|
||||||
|
if (!RsLocalConfig.instance) {
|
||||||
|
RsLocalConfig.instance = new RsLocalConfig()
|
||||||
|
await RsLocalConfig.instance.readConfig()
|
||||||
|
}
|
||||||
|
return RsLocalConfig.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readConfig() {
|
||||||
|
const basepath = process.cwd()
|
||||||
|
const filepath = path.join(basepath, ".roadsignrc")
|
||||||
|
if (!fs.existsSync(filepath)) {
|
||||||
|
throw new Error(`.roadsignrc file was not found at ${filepath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fs.readFileSync(filepath, "utf8")
|
||||||
|
this.config = JSON.parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async writeConfig() {
|
||||||
|
const basepath = process.cwd()
|
||||||
|
const filepath = path.join(basepath, ".roadsignrc")
|
||||||
|
fs.writeFileSync(filepath, JSON.stringify(this.config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RsLocalConfig, type RsLocalConfigData, type RsLocalConfigDeploymentPostActionData }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as os from "node:os"
|
import * as os from "node:os"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import * as fs from "node:fs/promises"
|
import * as fs from "node:fs"
|
||||||
|
|
||||||
interface RsConfigData {
|
interface RsConfigData {
|
||||||
servers: RsConfigServerData[]
|
servers: RsConfigServerData[]
|
||||||
@@ -33,18 +33,18 @@ class RsConfig {
|
|||||||
public async readConfig() {
|
public async readConfig() {
|
||||||
const basepath = os.homedir()
|
const basepath = os.homedir()
|
||||||
const filepath = path.join(basepath, ".roadsignrc")
|
const filepath = path.join(basepath, ".roadsignrc")
|
||||||
if (!await fs.exists(filepath)) {
|
if (!fs.existsSync(filepath)) {
|
||||||
await fs.writeFile(filepath, JSON.stringify(this.config))
|
fs.writeFileSync(filepath, JSON.stringify(this.config))
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fs.readFile(filepath, "utf8")
|
const data = fs.readFileSync(filepath, "utf8")
|
||||||
this.config = JSON.parse(data)
|
this.config = JSON.parse(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async writeConfig() {
|
public async writeConfig() {
|
||||||
const basepath = os.homedir()
|
const basepath = os.homedir()
|
||||||
const filepath = path.join(basepath, ".roadsignrc")
|
const filepath = path.join(basepath, ".roadsignrc")
|
||||||
await fs.writeFile(filepath, JSON.stringify(this.config))
|
fs.writeFileSync(filepath, JSON.stringify(this.config))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
cli/test/static-files.toml
Normal file
9
cli/test/static-files.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id = "static-files-num2"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "static-files-loc-num2"
|
||||||
|
hosts = ["127.0.0.1:8000"]
|
||||||
|
paths = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "static-files-des-num2"
|
||||||
|
uri = "files://../data/static-files"
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Enable latest features
|
"lib": ["ESNext"],
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "NodeNext",
|
||||||
"moduleDetection": "force",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
// Best practices
|
// Best practices
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -22,6 +21,7 @@
|
|||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"useUnknownInCatchVariables": false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ id = "example-region"
|
|||||||
|
|
||||||
[[locations]]
|
[[locations]]
|
||||||
id = "example"
|
id = "example"
|
||||||
host = ["localhost:8000"]
|
hosts = ["localhost:8000"]
|
||||||
path = ["/"]
|
paths = ["/"]
|
||||||
[[locations.destinations]]
|
[[locations.destinations]]
|
||||||
id = "example-destination"
|
id = "example-destination"
|
||||||
uri = "https://example.com"
|
uri = "https://example.com"
|
||||||
helmet = { x_frame_options = "SAMEORIGIN" }
|
helmet = { x_frame_options = "SAMEORIGIN" }
|
||||||
|
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -28,6 +28,7 @@ require (
|
|||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/net v0.29.0 // indirect
|
golang.org/x/net v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -169,6 +169,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func main() {
|
|||||||
// Configure settings
|
// Configure settings
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
viper.AddConfigPath("..")
|
viper.AddConfigPath("..")
|
||||||
|
viper.AddConfigPath("/")
|
||||||
viper.SetConfigName("settings")
|
viper.SetConfigName("settings")
|
||||||
viper.SetConfigType("toml")
|
viper.SetConfigType("toml")
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ func main() {
|
|||||||
log.Warn().Msgf("RoadSign auto generated api credential is %s", credential)
|
log.Warn().Msgf("RoadSign auto generated api credential is %s", credential)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize access logging
|
||||||
|
navi.InitializeLogging()
|
||||||
|
|
||||||
// Load & init navigator
|
// Load & init navigator
|
||||||
if err := navi.ReadInConfig(viper.GetString("paths.configs")); err != nil {
|
if err := navi.ReadInConfig(viper.GetString("paths.configs")); err != nil {
|
||||||
log.Panic().Err(err).Msg("An error occurred when loading configurations.")
|
log.Panic().Err(err).Msg("An error occurred when loading configurations.")
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package conn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var CliCommands = []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "list",
|
|
||||||
Aliases: []string{"ls"},
|
|
||||||
Description: "List all connected remote server",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
var servers []CliConnection
|
|
||||||
raw, _ := json.Marshal(viper.Get("servers"))
|
|
||||||
_ = json.Unmarshal(raw, &servers)
|
|
||||||
|
|
||||||
log.Info().Msgf("There are %d server(s) connected in total.", len(servers))
|
|
||||||
for idx, server := range servers {
|
|
||||||
log.Info().Msgf("%d) %s: %s", idx+1, server.ID, server.Url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "connect",
|
|
||||||
Aliases: []string{"add"},
|
|
||||||
Description: "Connect and save configuration of remote server",
|
|
||||||
ArgsUsage: "<id> <server url> <credential>",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
if ctx.Args().Len() < 3 {
|
|
||||||
return fmt.Errorf("must have three arguments: <id> <server url> <credential>")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := CliConnection{
|
|
||||||
ID: ctx.Args().Get(0),
|
|
||||||
Url: ctx.Args().Get(1),
|
|
||||||
Credential: ctx.Args().Get(2),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.CheckConnectivity(); err != nil {
|
|
||||||
return fmt.Errorf("couldn't connect server: %s", err.Error())
|
|
||||||
} else {
|
|
||||||
var servers []CliConnection
|
|
||||||
raw, _ := json.Marshal(viper.Get("servers"))
|
|
||||||
_ = json.Unmarshal(raw, &servers)
|
|
||||||
viper.Set("servers", append(servers, c))
|
|
||||||
|
|
||||||
if err := viper.WriteConfig(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
log.Info().Msg("Successfully connected a new remote server, enter \"rds ls\" to get more info.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "disconnect",
|
|
||||||
Aliases: []string{"remove"},
|
|
||||||
Description: "Remove a remote server configuration",
|
|
||||||
ArgsUsage: "<id>",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
if ctx.Args().Len() < 1 {
|
|
||||||
return fmt.Errorf("must have more one arguments: <server url>")
|
|
||||||
}
|
|
||||||
|
|
||||||
var servers []CliConnection
|
|
||||||
raw, _ := json.Marshal(viper.Get("servers"))
|
|
||||||
_ = json.Unmarshal(raw, &servers)
|
|
||||||
viper.Set("servers", lo.Filter(servers, func(item CliConnection, idx int) bool {
|
|
||||||
return item.ID != ctx.Args().Get(0)
|
|
||||||
}))
|
|
||||||
|
|
||||||
if err := viper.WriteConfig(); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
log.Info().Msg("Successfully disconnected a remote server, enter \"rds ls\" to get more info.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package conn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
roadsign "git.solsynth.dev/goatworks/roadsign/pkg"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CliConnection struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Credential string `json:"credential"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v CliConnection) CheckConnectivity() error {
|
|
||||||
client := fiber.Get(v.Url + "/cgi/metadata")
|
|
||||||
client.BasicAuth("RoadSign CLI", v.Credential)
|
|
||||||
|
|
||||||
if status, data, err := client.Bytes(); len(err) > 0 {
|
|
||||||
return fmt.Errorf("couldn't connect to server: %q", err)
|
|
||||||
} else if status != 200 {
|
|
||||||
return fmt.Errorf("server rejected request, may cause by invalid credential")
|
|
||||||
} else {
|
|
||||||
var resp fiber.Map
|
|
||||||
if err := json.Unmarshal(data, &resp); err != nil {
|
|
||||||
return err
|
|
||||||
} else if resp["server"] != "RoadSign" {
|
|
||||||
return fmt.Errorf("remote server isn't roadsign")
|
|
||||||
} else if resp["version"] != roadsign.AppVersion {
|
|
||||||
if strings.Contains(roadsign.AppVersion, "#") {
|
|
||||||
return fmt.Errorf("remote server version mismatch client version, update or downgrade client required")
|
|
||||||
} else {
|
|
||||||
log.Warn().Msg("RoadSign CLI didn't complied with vcs information, compatibility was disabled. To enable it, reinstall cli with -buildvcs flag.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package conn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetConnection(id string) (CliConnection, bool) {
|
|
||||||
var servers []CliConnection
|
|
||||||
raw, _ := json.Marshal(viper.Get("servers"))
|
|
||||||
_ = json.Unmarshal(raw, &servers)
|
|
||||||
return lo.Find(servers, func(item CliConnection) bool {
|
|
||||||
return item.ID == id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package deploy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
|
||||||
|
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/conn"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DeployCommands = []*cli.Command{
|
|
||||||
{
|
|
||||||
Name: "deploy",
|
|
||||||
Aliases: []string{"dp"},
|
|
||||||
ArgsUsage: "<server> <site> <upstream> [path]",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
if ctx.Args().Len() < 4 {
|
|
||||||
return fmt.Errorf("must have four arguments: <server> <site> <upstream> <path>")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasSuffix(ctx.Args().Get(3), ".zip") {
|
|
||||||
return fmt.Errorf("input file must be a zip file and ends with .zip")
|
|
||||||
}
|
|
||||||
|
|
||||||
server, ok := conn.GetConnection(ctx.Args().Get(0))
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
|
|
||||||
} else if err := server.CheckConnectivity(); err != nil {
|
|
||||||
return fmt.Errorf("couldn't connect server: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send request
|
|
||||||
log.Info().Msg("Now publishing to remote server...")
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/webhooks/publish/%s/%s?mimetype=%s", ctx.Args().Get(1), ctx.Args().Get(2), "application/zip")
|
|
||||||
client := fiber.Put(server.Url+url).
|
|
||||||
SendFile(ctx.Args().Get(3), "attachments").
|
|
||||||
MultipartForm(nil).
|
|
||||||
BasicAuth("RoadSign CLI", server.Credential)
|
|
||||||
|
|
||||||
if status, data, err := client.Bytes(); len(err) > 0 {
|
|
||||||
return fmt.Errorf("failed to publish to remote: %q", err)
|
|
||||||
} else if status != 200 {
|
|
||||||
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("Well done! Your site is successfully published! 🎉")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "sync",
|
|
||||||
ArgsUsage: "<server> <site> <configuration path>",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
if ctx.Args().Len() < 3 {
|
|
||||||
return fmt.Errorf("must have three arguments: <server> <site> <configuration path>")
|
|
||||||
}
|
|
||||||
|
|
||||||
server, ok := conn.GetConnection(ctx.Args().Get(0))
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
|
|
||||||
} else if err := server.CheckConnectivity(); err != nil {
|
|
||||||
return fmt.Errorf("couldn't connect server: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var raw []byte
|
|
||||||
if file, err := os.Open(ctx.Args().Get(2)); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
raw, _ = io.ReadAll(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/webhooks/sync/%s", ctx.Args().Get(1))
|
|
||||||
client := fiber.Put(server.Url+url).
|
|
||||||
JSONEncoder(jsoniter.ConfigCompatibleWithStandardLibrary.Marshal).
|
|
||||||
JSONDecoder(jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal).
|
|
||||||
Body(raw).
|
|
||||||
BasicAuth("RoadSign CLI", server.Credential)
|
|
||||||
|
|
||||||
if status, data, err := client.Bytes(); len(err) > 0 {
|
|
||||||
return fmt.Errorf("failed to sync to remote: %q", err)
|
|
||||||
} else if status != 200 {
|
|
||||||
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("Well done! Your site configuration is up-to-date! 🎉")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
roadsign "git.solsynth.dev/goatworks/roadsign/pkg"
|
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/conn"
|
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/cmd/rdc/deploy"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Configure settings
|
|
||||||
viper.AddConfigPath("$HOME")
|
|
||||||
viper.SetConfigName(".roadsignrc")
|
|
||||||
viper.SetConfigType("toml")
|
|
||||||
|
|
||||||
// Load settings
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
|
||||||
viper.SafeWriteConfig()
|
|
||||||
viper.ReadInConfig()
|
|
||||||
} else {
|
|
||||||
log.Panic().Err(err).Msg("An error occurred when loading settings.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure CLI
|
|
||||||
app := &cli.App{
|
|
||||||
Name: "RoadSign CLI",
|
|
||||||
Version: roadsign.AppVersion,
|
|
||||||
Suggest: true,
|
|
||||||
Commands: append(append([]*cli.Command{}, conn.CliCommands...), deploy.DeployCommands...),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run CLI
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("An error occurred when running cli.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,7 @@ func ReadInConfig(root string) error {
|
|||||||
instance := &RoadApp{
|
instance := &RoadApp{
|
||||||
Regions: make([]*Region, 0),
|
Regions: make([]*Region, 0),
|
||||||
Metrics: &RoadMetrics{
|
Metrics: &RoadMetrics{
|
||||||
Traces: make([]RoadTrace, 0),
|
|
||||||
Traffic: make(map[string]int64),
|
Traffic: make(map[string]int64),
|
||||||
TrafficFrom: make(map[string]int64),
|
|
||||||
TotalTraffic: 0,
|
TotalTraffic: 0,
|
||||||
StartupAt: time.Now(),
|
StartupAt: time.Now(),
|
||||||
},
|
},
|
||||||
|
|||||||
20
pkg/navi/logging.go
Normal file
20
pkg/navi/logging.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package navi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var accessLogger *log.Logger
|
||||||
|
|
||||||
|
func InitializeLogging() {
|
||||||
|
accessLogger = log.New(&lumberjack.Logger{
|
||||||
|
Filename: viper.GetString("logging.access"),
|
||||||
|
MaxSize: 10,
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 30,
|
||||||
|
Compress: true,
|
||||||
|
}, "", 0)
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package navi
|
package navi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/viper"
|
"bufio"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoadMetrics struct {
|
type RoadMetrics struct {
|
||||||
Traces []RoadTrace `json:"-"`
|
|
||||||
|
|
||||||
Traffic map[string]int64 `json:"traffic"`
|
Traffic map[string]int64 `json:"traffic"`
|
||||||
TrafficFrom map[string]int64 `json:"traffic_from"`
|
|
||||||
TotalTraffic int64 `json:"total_traffic"`
|
TotalTraffic int64 `json:"total_traffic"`
|
||||||
StartupAt time.Time `json:"startup_at"`
|
StartupAt time.Time `json:"startup_at"`
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,10 @@ type RoadTraceError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *RoadMetrics) AddTrace(trace RoadTrace) {
|
func (v *RoadMetrics) AddTrace(trace RoadTrace) {
|
||||||
|
if viper.GetBool("performance.low_memory") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
v.TotalTraffic++
|
v.TotalTraffic++
|
||||||
trace.Timestamp = time.Now()
|
trace.Timestamp = time.Now()
|
||||||
if _, ok := v.Traffic[trace.Region]; !ok {
|
if _, ok := v.Traffic[trace.Region]; !ok {
|
||||||
@@ -38,22 +43,28 @@ func (v *RoadMetrics) AddTrace(trace RoadTrace) {
|
|||||||
} else {
|
} else {
|
||||||
v.Traffic[trace.Region]++
|
v.Traffic[trace.Region]++
|
||||||
}
|
}
|
||||||
if _, ok := v.TrafficFrom[trace.IpAddress]; !ok {
|
|
||||||
v.TrafficFrom[trace.IpAddress] = 0
|
|
||||||
} else {
|
|
||||||
v.TrafficFrom[trace.IpAddress]++
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Traces = append(v.Traces, trace)
|
raw, _ := jsoniter.Marshal(trace)
|
||||||
|
accessLogger.Println(string(raw))
|
||||||
// Garbage recycle
|
}
|
||||||
if len(v.Traffic) > viper.GetInt("performance.traces_limit") {
|
|
||||||
v.Traffic = make(map[string]int64)
|
func (v *RoadMetrics) ReadTrace() []RoadTrace {
|
||||||
}
|
fp := viper.GetString("logging.access")
|
||||||
if len(v.TrafficFrom) > viper.GetInt("performance.traces_limit") {
|
file, err := os.Open(fp)
|
||||||
v.TrafficFrom = make(map[string]int64)
|
if err != nil {
|
||||||
}
|
return nil
|
||||||
if len(v.Traces) > viper.GetInt("performance.traces_limit") {
|
}
|
||||||
v.Traces = v.Traces[1:]
|
defer file.Close()
|
||||||
}
|
|
||||||
|
var out []RoadTrace
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
var entry RoadTrace
|
||||||
|
if err := jsoniter.Unmarshal([]byte(line), &entry); err == nil {
|
||||||
|
out = append(out, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,10 +148,10 @@ func makeFileResponse(c *fiber.Ctx, dest *Destination) error {
|
|||||||
return fmt.Errorf("failed to stat: %w", err)
|
return fmt.Errorf("failed to stat: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve index if path is directory
|
// Serve index if the path is a directory
|
||||||
if stat.IsDir() {
|
if stat.IsDir() {
|
||||||
indexFile := lo.Ternary(len(queries.Get("index")) > 0, queries.Get("index"), "index.html")
|
indexFile := lo.Ternary(len(queries.Get("index")) > 0, queries.Get("index"), "index.html")
|
||||||
indexPath := utils.TrimRight(path, '/') + indexFile
|
indexPath := filepath.Join(path, indexFile)
|
||||||
index, err := root.Open(indexPath)
|
index, err := root.Open(indexPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
indexStat, err := index.Stat()
|
indexStat, err := index.Stat()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (v *Destination) GetType() DestinationType {
|
|||||||
func (v *Destination) GetRawUri() (string, url.Values) {
|
func (v *Destination) GetRawUri() (string, url.Values) {
|
||||||
uri := strings.SplitN(v.Uri, "://", 2)[1]
|
uri := strings.SplitN(v.Uri, "://", 2)[1]
|
||||||
data := strings.SplitN(uri, "?", 2)
|
data := strings.SplitN(uri, "?", 2)
|
||||||
data = append(data, " ") // Make the data array least have two elements
|
data = append(data, " ") // Make the data array at least have two elements
|
||||||
qs, _ := url.ParseQuery(data[1])
|
qs, _ := url.ParseQuery(data[1])
|
||||||
|
|
||||||
return data[0], qs
|
return data[0], qs
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package navi
|
package navi
|
||||||
|
|
||||||
import "git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
import (
|
||||||
|
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
func InitializeWarden(regions []*Region) {
|
func InitializeWarden(regions []*Region) {
|
||||||
pool := make([]*warden.AppInstance, 0)
|
pool := make([]*warden.AppInstance, 0)
|
||||||
|
|
||||||
|
log.Info().Msg("Starting Warden applications...")
|
||||||
|
|
||||||
for _, region := range regions {
|
for _, region := range regions {
|
||||||
for _, application := range region.Applications {
|
for _, application := range region.Applications {
|
||||||
pool = append(pool, &warden.AppInstance{
|
pool = append(pool, &warden.AppInstance{
|
||||||
@@ -15,5 +20,7 @@ func InitializeWarden(regions []*Region) {
|
|||||||
|
|
||||||
// Hot swap
|
// Hot swap
|
||||||
warden.InstancePool = pool
|
warden.InstancePool = pool
|
||||||
warden.StartPool()
|
errs := warden.StartPool()
|
||||||
|
|
||||||
|
log.Info().Any("errs", errs).Msg("Warden applications has been started.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ func getTraffic(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTraces(c *fiber.Ctx) error {
|
func getTraces(c *fiber.Ctx) error {
|
||||||
return c.JSON(navi.R.Metrics.Traces)
|
return c.JSON(navi.R.Metrics.ReadTrace())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package sideload
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/navi"
|
"git.solsynth.dev/goatworks/roadsign/pkg/navi"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -42,7 +46,9 @@ func doPublish(c *fiber.Ctx) error {
|
|||||||
var instance *warden.AppInstance
|
var instance *warden.AppInstance
|
||||||
if application != nil {
|
if application != nil {
|
||||||
if instance = warden.GetFromPool(application.ID); instance != nil {
|
if instance = warden.GetFromPool(application.ID); instance != nil {
|
||||||
_ = instance.Stop()
|
if err := instance.Stop(); err != nil {
|
||||||
|
log.Warn().Err(err).Str("id", application.ID).Msg("Failed to stop application when publishing...")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if destination != nil && destination.GetType() != navi.DestinationStaticFile {
|
} else if destination != nil && destination.GetType() != navi.DestinationStaticFile {
|
||||||
return fiber.ErrUnprocessableEntity
|
return fiber.ErrUnprocessableEntity
|
||||||
@@ -50,7 +56,7 @@ func doPublish(c *fiber.Ctx) error {
|
|||||||
return fiber.ErrNotFound
|
return fiber.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Query("overwrite", "yes") == "yes" {
|
if c.QueryBool("overwrite", true) {
|
||||||
files, _ := filepath.Glob(filepath.Join(workdir, "*"))
|
files, _ := filepath.Glob(filepath.Join(workdir, "*"))
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
_ = os.Remove(file)
|
_ = os.Remove(file)
|
||||||
@@ -74,6 +80,7 @@ func doPublish(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ = os.Remove(dst)
|
||||||
default:
|
default:
|
||||||
dst := filepath.Join(workdir, file.Filename)
|
dst := filepath.Join(workdir, file.Filename)
|
||||||
if err := c.SaveFile(file, dst); err != nil {
|
if err := c.SaveFile(file, dst); err != nil {
|
||||||
@@ -83,6 +90,15 @@ func doPublish(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if postScript := c.FormValue("post-deploy-script", ""); len(postScript) > 0 {
|
||||||
|
cmd := exec.Command("sh", "-c", postScript)
|
||||||
|
cmd.Dir = filepath.Join(workdir)
|
||||||
|
cmd.Env = append(cmd.Env, strings.Split(c.FormValue("post-deploy-environment", ""), "\n")...)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("post deploy script runs failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if instance != nil {
|
if instance != nil {
|
||||||
_ = instance.Wake()
|
_ = instance.Wake()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,11 @@ func doSync(c *fiber.Ctx) error {
|
|||||||
if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil {
|
if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil {
|
||||||
return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error())
|
return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error())
|
||||||
} else {
|
} else {
|
||||||
raw, _ := toml.Marshal(req)
|
var testOut map[string]any
|
||||||
file.Write(raw)
|
if err := toml.Unmarshal([]byte(req), &testOut); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid configuration: %v", err))
|
||||||
|
}
|
||||||
|
_, _ = file.Write([]byte(req))
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +77,7 @@ func doSync(c *fiber.Ctx) error {
|
|||||||
_ = instance.Stop()
|
_ = instance.Stop()
|
||||||
}
|
}
|
||||||
for _, instance := range startQueue {
|
for _, instance := range startQueue {
|
||||||
_ = instance.Start()
|
_ = instance.Wake()
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package sideload
|
package sideload
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/navi"
|
"git.solsynth.dev/goatworks/roadsign/pkg/navi"
|
||||||
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
"git.solsynth.dev/goatworks/roadsign/pkg/warden"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getStats(c *fiber.Ctx) error {
|
func getStats(c *fiber.Ctx) error {
|
||||||
@@ -27,7 +28,6 @@ func getStats(c *fiber.Ctx) error {
|
|||||||
"uptime": time.Since(navi.R.Metrics.StartupAt).Milliseconds(),
|
"uptime": time.Since(navi.R.Metrics.StartupAt).Milliseconds(),
|
||||||
"traffic": fiber.Map{
|
"traffic": fiber.Map{
|
||||||
"total": navi.R.Metrics.TotalTraffic,
|
"total": navi.R.Metrics.TotalTraffic,
|
||||||
"unique_client": len(navi.R.Metrics.TrafficFrom),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package warden
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/rs/zerolog/log"
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,11 +47,12 @@ const (
|
|||||||
|
|
||||||
type AppInstance struct {
|
type AppInstance struct {
|
||||||
Manifest Application `json:"manifest"`
|
Manifest Application `json:"manifest"`
|
||||||
|
Status AppStatus `json:"status"`
|
||||||
|
|
||||||
Cmd *exec.Cmd `json:"-"`
|
Cmd *exec.Cmd `json:"-"`
|
||||||
Logger strings.Builder `json:"-"`
|
|
||||||
|
|
||||||
Status AppStatus `json:"status"`
|
LogPath string `json:"-"`
|
||||||
|
Logger *lumberjack.Logger `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *AppInstance) Wake() error {
|
func (v *AppInstance) Wake() error {
|
||||||
@@ -62,14 +67,8 @@ func (v *AppInstance) Wake() error {
|
|||||||
}
|
}
|
||||||
if v.Cmd.ProcessState.Exited() {
|
if v.Cmd.ProcessState.Exited() {
|
||||||
return v.Start()
|
return v.Start()
|
||||||
} else if v.Cmd.ProcessState.Exited() {
|
|
||||||
return fmt.Errorf("process already dead")
|
|
||||||
}
|
}
|
||||||
if v.Cmd.ProcessState.Exited() {
|
|
||||||
return fmt.Errorf("cannot start process")
|
|
||||||
} else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *AppInstance) Start() error {
|
func (v *AppInstance) Start() error {
|
||||||
@@ -82,8 +81,21 @@ func (v *AppInstance) Start() error {
|
|||||||
v.Cmd = exec.Command(manifest.Command[0], manifest.Command[1:]...)
|
v.Cmd = exec.Command(manifest.Command[0], manifest.Command[1:]...)
|
||||||
v.Cmd.Dir = filepath.Join(manifest.Workdir)
|
v.Cmd.Dir = filepath.Join(manifest.Workdir)
|
||||||
v.Cmd.Env = append(v.Cmd.Env, manifest.Environment...)
|
v.Cmd.Env = append(v.Cmd.Env, manifest.Environment...)
|
||||||
v.Cmd.Stdout = &v.Logger
|
|
||||||
v.Cmd.Stderr = &v.Logger
|
logBasePath := viper.GetString("logging.warden_apps")
|
||||||
|
logPath := filepath.Join(logBasePath, fmt.Sprintf("%s.log", manifest.ID))
|
||||||
|
|
||||||
|
v.LogPath = logPath
|
||||||
|
v.Logger = &lumberjack.Logger{
|
||||||
|
Filename: v.LogPath,
|
||||||
|
MaxSize: 10,
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 30,
|
||||||
|
Compress: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Cmd.Stdout = v.Logger
|
||||||
|
v.Cmd.Stderr = v.Logger
|
||||||
|
|
||||||
// Monitor
|
// Monitor
|
||||||
go func() {
|
go func() {
|
||||||
@@ -93,7 +105,7 @@ func (v *AppInstance) Start() error {
|
|||||||
} else if v.Cmd != nil && v.Cmd.ProcessState == nil {
|
} else if v.Cmd != nil && v.Cmd.ProcessState == nil {
|
||||||
v.Status = AppStarted
|
v.Status = AppStarted
|
||||||
} else {
|
} else {
|
||||||
v.Status = lo.Ternary(v.Cmd == nil, AppExited, AppFailure)
|
v.Status = AppFailure
|
||||||
v.Cmd = nil
|
v.Cmd = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -110,18 +122,28 @@ func (v *AppInstance) Stop() error {
|
|||||||
log.Warn().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to send SIGTERM to process...")
|
log.Warn().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to send SIGTERM to process...")
|
||||||
if err = v.Cmd.Process.Kill(); err != nil {
|
if err = v.Cmd.Process.Kill(); err != nil {
|
||||||
log.Error().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to kill process...")
|
log.Error().Int("pid", v.Cmd.Process.Pid).Err(err).Msgf("Failed to kill process...")
|
||||||
} else {
|
|
||||||
v.Cmd = nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
v.Cmd = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to wait for the process to exit
|
||||||
|
// The wait syscall will read the exit status of the process
|
||||||
|
// So that we don't produce defunct processes
|
||||||
|
// Refer to https://stackoverflow.com/questions/46293435/golang-exec-command-cause-a-lot-of-defunct-processes
|
||||||
|
_ = v.Cmd.Wait()
|
||||||
|
|
||||||
|
v.Cmd = nil
|
||||||
|
v.Status = AppExited
|
||||||
|
v.Logger.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *AppInstance) Logs() string {
|
func (v *AppInstance) Logs() string {
|
||||||
return v.Logger.String()
|
file, err := os.Open(v.LogPath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
raw, _ := io.ReadAll(file)
|
||||||
|
return string(raw)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ force_https = false
|
|||||||
max_body_size = 549_755_813_888 # 512 GiB
|
max_body_size = 549_755_813_888 # 512 GiB
|
||||||
max_qps = -1
|
max_qps = -1
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
access = "./logs/access.log"
|
||||||
|
warden_apps = "./logs/warden"
|
||||||
|
|
||||||
[paths]
|
[paths]
|
||||||
configs = "./config"
|
configs = "./config"
|
||||||
|
|
||||||
@@ -29,7 +33,7 @@ request_logging = true
|
|||||||
capture_traces = true
|
capture_traces = true
|
||||||
|
|
||||||
[performance]
|
[performance]
|
||||||
traces_limit = 256
|
low_memory = false
|
||||||
prefork = false
|
prefork = false
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
|
|||||||
1
test/data/.gitignore
vendored
1
test/data/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
|
/spa
|
||||||
/capital
|
/capital
|
||||||
/static-files
|
/static-files
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
id = "example"
|
id = "static-files"
|
||||||
|
|
||||||
[[locations]]
|
[[locations]]
|
||||||
id = "example-location"
|
id = "static-files-loc"
|
||||||
host = ["localhost:8000"]
|
hosts = ["localhost:8000"]
|
||||||
path = ["/"]
|
paths = ["/"]
|
||||||
[[locations.destinations]]
|
[[locations.destinations]]
|
||||||
id = "example-destination"
|
id = "static-files-des"
|
||||||
uri = "files://../data/spa?fallback=index.html"
|
uri = "files://../data/spa?fallback=index.html"
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
|
id = "central-dc"
|
||||||
|
|
||||||
[debug]
|
[debug]
|
||||||
print_routes = true
|
print_routes = false
|
||||||
|
|
||||||
|
[sideload]
|
||||||
|
ports = [":81"]
|
||||||
|
secured_ports = []
|
||||||
|
trusted_proxies = ["localhost"]
|
||||||
|
|
||||||
[hypertext]
|
[hypertext]
|
||||||
sideload_ports = [":81"]
|
|
||||||
sideload_secured_ports = []
|
|
||||||
ports = [":8000"]
|
ports = [":8000"]
|
||||||
secured_ports = []
|
secured_ports = []
|
||||||
|
force_https = false
|
||||||
|
|
||||||
[hypertext.certificate]
|
# [[hypertext.certificate]]
|
||||||
redirect = false
|
# key = "./certs/privkey.pem"
|
||||||
sideload_key = "./cert.key"
|
# pem = "./certs/fullchain.pem"
|
||||||
sideload_pem = "./cert.pem"
|
|
||||||
key = "./cert.key"
|
|
||||||
pem = "./cert.pem"
|
|
||||||
|
|
||||||
[hypertext.limitation]
|
[hypertext.limitation]
|
||||||
max_body_size = 549_755_813_888 # 512 GiB
|
max_body_size = 549_755_813_888 # 512 GiB
|
||||||
@@ -21,11 +24,13 @@ max_qps = -1
|
|||||||
[paths]
|
[paths]
|
||||||
configs = "./config"
|
configs = "./config"
|
||||||
|
|
||||||
[performance]
|
[telemetry]
|
||||||
request_logging = true
|
request_logging = true
|
||||||
network_timeout = 3_000
|
capture_traces = true
|
||||||
|
|
||||||
|
[performance]
|
||||||
|
traces_limit = 256
|
||||||
prefork = false
|
prefork = false
|
||||||
|
|
||||||
[security]
|
[security]
|
||||||
sideload_trusted_proxies = ["localhost"]
|
|
||||||
credential = "e81f43f32d934271af6322e5376f5f59"
|
credential = "e81f43f32d934271af6322e5376f5f59"
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ uri = "http://localhost:3000"
|
|||||||
[[applications]]
|
[[applications]]
|
||||||
id = "capital-app"
|
id = "capital-app"
|
||||||
workdir = "../data/capital"
|
workdir = "../data/capital"
|
||||||
command = ["node", "server/index.mjs"]
|
command = ["node", "standalone/server.js"]
|
||||||
environment = []
|
environment = []
|
||||||
@@ -21,6 +21,10 @@ force_https = false
|
|||||||
max_body_size = 549_755_813_888 # 512 GiB
|
max_body_size = 549_755_813_888 # 512 GiB
|
||||||
max_qps = -1
|
max_qps = -1
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
access = "../../logs/access.log"
|
||||||
|
warden_apps = "../../logs/warden"
|
||||||
|
|
||||||
[paths]
|
[paths]
|
||||||
configs = "./config"
|
configs = "./config"
|
||||||
|
|
||||||
|
|||||||
9
test/roadsign/config/static-files.toml
Executable file
9
test/roadsign/config/static-files.toml
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
id = "static-files-num2"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "static-files-loc-num2"
|
||||||
|
hosts = ["127.0.0.1:8000"]
|
||||||
|
paths = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "static-files-des-num2"
|
||||||
|
uri = "files://../data/static-files"
|
||||||
Reference in New Issue
Block a user