mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-13 05:40:38 +01:00
remove environment variable support and config file autogeneration
This commit is contained in:
parent
9253dd19ba
commit
c93b36c757
6 changed files with 17 additions and 271 deletions
|
@ -26,7 +26,6 @@ COPY --from=builder /app/.next ./.next
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/init ./dist/init
|
COPY --from=builder /app/init ./dist/init
|
||||||
|
|
||||||
COPY config/config.example.yml ./dist/config.example.yml
|
|
||||||
COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml
|
COPY config/traefik/traefik_config.example.yml ./dist/traefik_config.example.yml
|
||||||
COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml
|
COPY config/traefik/dynamic_config.example.yml ./dist/dynamic_config.example.yml
|
||||||
COPY server/db/names.json ./dist/names.json
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
http:
|
|
||||||
middlewares:
|
|
||||||
redirect-to-https:
|
|
||||||
redirectScheme:
|
|
||||||
scheme: https
|
|
||||||
|
|
||||||
routers:
|
|
||||||
# HTTP to HTTPS redirect router
|
|
||||||
main-app-router-redirect:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
|
||||||
service: next-service
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
middlewares:
|
|
||||||
- redirect-to-https
|
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
|
||||||
next-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
|
||||||
service: next-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
# API router (handles /api/v1 paths)
|
|
||||||
api-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
|
||||||
service: api-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
# WebSocket router
|
|
||||||
ws-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
|
||||||
service: api-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
services:
|
|
||||||
next-service:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://pangolin:{{.NEXT_PORT}}" # Next.js server
|
|
||||||
|
|
||||||
api-service:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://pangolin:{{.EXTERNAL_PORT}}" # API/WebSocket server
|
|
|
@ -1,44 +0,0 @@
|
||||||
api:
|
|
||||||
insecure: true
|
|
||||||
dashboard: true
|
|
||||||
|
|
||||||
providers:
|
|
||||||
http:
|
|
||||||
endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
|
|
||||||
pollInterval: "5s"
|
|
||||||
file:
|
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
|
||||||
|
|
||||||
experimental:
|
|
||||||
plugins:
|
|
||||||
badger:
|
|
||||||
moduleName: "github.com/fosrl/badger"
|
|
||||||
version: "v1.0.0-beta.3"
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: "INFO"
|
|
||||||
format: "common"
|
|
||||||
|
|
||||||
certificatesResolvers:
|
|
||||||
letsencrypt:
|
|
||||||
acme:
|
|
||||||
httpChallenge:
|
|
||||||
entryPoint: web
|
|
||||||
email: "{{.LetsEncryptEmail}}"
|
|
||||||
storage: "/letsencrypt/acme.json"
|
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: ":80"
|
|
||||||
websecure:
|
|
||||||
address: ":443"
|
|
||||||
transport:
|
|
||||||
respondingTimeouts:
|
|
||||||
readTimeout: "30m"
|
|
||||||
http:
|
|
||||||
tls:
|
|
||||||
certResolver: "letsencrypt"
|
|
||||||
|
|
||||||
serversTransport:
|
|
||||||
insecureSkipVerify: true
|
|
|
@ -1,11 +1,9 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import path from "path";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import {
|
||||||
__DIRNAME,
|
__DIRNAME,
|
||||||
APP_PATH,
|
|
||||||
APP_VERSION,
|
APP_VERSION,
|
||||||
configFilePath1,
|
configFilePath1,
|
||||||
configFilePath2
|
configFilePath2
|
||||||
|
@ -14,12 +12,6 @@ import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
import stoi from "./stoi";
|
import stoi from "./stoi";
|
||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
// const hostnameSchema = z
|
|
||||||
// .string()
|
|
||||||
// .regex(
|
|
||||||
// /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/
|
|
||||||
// )
|
|
||||||
// .or(z.literal("localhost"));
|
|
||||||
|
|
||||||
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
|
||||||
return process.env[envVar] ?? valFromYaml;
|
return process.env[envVar] ?? valFromYaml;
|
||||||
|
@ -31,7 +23,6 @@ const configSchema = z.object({
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.optional()
|
.optional()
|
||||||
.transform(getEnvOrYaml("APP_DASHBOARDURL"))
|
|
||||||
.pipe(z.string().url())
|
.pipe(z.string().url())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase()),
|
||||||
log_level: z.enum(["debug", "info", "warn", "error"]),
|
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||||
|
@ -63,37 +54,11 @@ const configSchema = z.object({
|
||||||
{
|
{
|
||||||
message: "At least one domain must be defined"
|
message: "At least one domain must be defined"
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(domains) => {
|
|
||||||
const envBaseDomain = process.env.APP_BASE_DOMAIN;
|
|
||||||
|
|
||||||
if (envBaseDomain) {
|
|
||||||
return z.string().nonempty().safeParse(envBaseDomain).success;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "APP_BASE_DOMAIN must be a valid hostname"
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
server: z.object({
|
server: z.object({
|
||||||
external_port: portSchema
|
external_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.optional()
|
internal_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.transform(getEnvOrYaml("SERVER_EXTERNALPORT"))
|
next_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
internal_port: portSchema
|
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("SERVER_INTERNALPORT"))
|
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
next_port: portSchema
|
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("SERVER_NEXTPORT"))
|
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_access_token_param: z.string(),
|
resource_access_token_param: z.string(),
|
||||||
|
@ -126,15 +91,10 @@ const configSchema = z.object({
|
||||||
additional_middlewares: z.array(z.string()).optional()
|
additional_middlewares: z.array(z.string()).optional()
|
||||||
}),
|
}),
|
||||||
gerbil: z.object({
|
gerbil: z.object({
|
||||||
start_port: portSchema
|
start_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("GERBIL_STARTPORT"))
|
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
base_endpoint: z
|
base_endpoint: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform(getEnvOrYaml("GERBIL_BASEENDPOINT"))
|
|
||||||
.pipe(z.string())
|
.pipe(z.string())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase()),
|
||||||
use_subdomain: z.boolean(),
|
use_subdomain: z.boolean(),
|
||||||
|
@ -197,10 +157,6 @@ export class Config {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
|
|
||||||
if (process.env.GENERATE_TRAEFIK_CONFIG === "true") {
|
|
||||||
this.createTraefikConfig();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadConfig() {
|
public loadConfig() {
|
||||||
|
@ -226,44 +182,9 @@ export class Config {
|
||||||
environment = loadConfig(configFilePath2);
|
environment = loadConfig(configFilePath2);
|
||||||
}
|
}
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
const exampleConfigPath = path.join(
|
throw new Error(
|
||||||
__DIRNAME,
|
"No configuration file found. Please create one. https://docs.fossorial.io/"
|
||||||
"config.example.yml"
|
|
||||||
);
|
);
|
||||||
if (fs.existsSync(exampleConfigPath)) {
|
|
||||||
try {
|
|
||||||
const exampleConfigContent = fs.readFileSync(
|
|
||||||
exampleConfigPath,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
configFilePath1,
|
|
||||||
exampleConfigContent,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
environment = loadConfig(configFilePath1);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"See the docs for information about what to include in the configuration file: https://docs.fossorial.io/Pangolin/Configuration/config"
|
|
||||||
);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new Error(
|
|
||||||
`Error creating configuration file from example: ${
|
|
||||||
error.message
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"No configuration file found and no example configuration available"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!environment) {
|
|
||||||
throw new Error("No configuration file found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedConfig = configSchema.safeParse(environment);
|
const parsedConfig = configSchema.safeParse(environment);
|
||||||
|
@ -309,17 +230,6 @@ export class Config {
|
||||||
: "false";
|
: "false";
|
||||||
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
|
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
|
||||||
|
|
||||||
if (process.env.APP_BASE_DOMAIN) {
|
|
||||||
console.log(
|
|
||||||
`DEPRECATED! APP_BASE_DOMAIN is deprecated and will be removed in a future release. Use the domains section in the configuration file instead. See https://docs.fossorial.io/Pangolin/Configuration/config for more information.`
|
|
||||||
);
|
|
||||||
|
|
||||||
parsedConfig.data.domains.domain1 = {
|
|
||||||
base_domain: process.env.APP_BASE_DOMAIN,
|
|
||||||
cert_resolver: "letsencrypt"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rawConfig = parsedConfig.data;
|
this.rawConfig = parsedConfig.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -336,72 +246,6 @@ export class Config {
|
||||||
public getDomain(domainId: string) {
|
public getDomain(domainId: string) {
|
||||||
return this.rawConfig.domains[domainId];
|
return this.rawConfig.domains[domainId];
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTraefikConfig() {
|
|
||||||
try {
|
|
||||||
// check if traefik_config.yml and dynamic_config.yml exists in APP_PATH/traefik
|
|
||||||
const defaultTraefikConfigPath = path.join(
|
|
||||||
__DIRNAME,
|
|
||||||
"traefik_config.example.yml"
|
|
||||||
);
|
|
||||||
const defaultDynamicConfigPath = path.join(
|
|
||||||
__DIRNAME,
|
|
||||||
"dynamic_config.example.yml"
|
|
||||||
);
|
|
||||||
|
|
||||||
const traefikPath = path.join(APP_PATH, "traefik");
|
|
||||||
if (!fs.existsSync(traefikPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load default configs
|
|
||||||
let traefikConfig = fs.readFileSync(
|
|
||||||
defaultTraefikConfigPath,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
let dynamicConfig = fs.readFileSync(
|
|
||||||
defaultDynamicConfigPath,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
traefikConfig = traefikConfig
|
|
||||||
.split("{{.LetsEncryptEmail}}")
|
|
||||||
.join(this.rawConfig.users.server_admin.email);
|
|
||||||
traefikConfig = traefikConfig
|
|
||||||
.split("{{.INTERNAL_PORT}}")
|
|
||||||
.join(this.rawConfig.server.internal_port.toString());
|
|
||||||
|
|
||||||
dynamicConfig = dynamicConfig
|
|
||||||
.split("{{.DashboardDomain}}")
|
|
||||||
.join(new URL(this.rawConfig.app.dashboard_url).hostname);
|
|
||||||
dynamicConfig = dynamicConfig
|
|
||||||
.split("{{.NEXT_PORT}}")
|
|
||||||
.join(this.rawConfig.server.next_port.toString());
|
|
||||||
dynamicConfig = dynamicConfig
|
|
||||||
.split("{{.EXTERNAL_PORT}}")
|
|
||||||
.join(this.rawConfig.server.external_port.toString());
|
|
||||||
|
|
||||||
// write thiese to the traefik directory
|
|
||||||
const traefikConfigPath = path.join(
|
|
||||||
traefikPath,
|
|
||||||
"traefik_config.yml"
|
|
||||||
);
|
|
||||||
const dynamicConfigPath = path.join(
|
|
||||||
traefikPath,
|
|
||||||
"dynamic_config.yml"
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(traefikConfigPath, traefikConfig, "utf8");
|
|
||||||
fs.writeFileSync(dynamicConfigPath, dynamicConfig, "utf8");
|
|
||||||
|
|
||||||
console.log("Traefik configuration files created");
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
"Failed to generate the Traefik configuration files. Please create them manually."
|
|
||||||
);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = new Config();
|
export const config = new Config();
|
||||||
|
|
|
@ -18,7 +18,7 @@ export async function clearStaleData() {
|
||||||
.delete(sessions)
|
.delete(sessions)
|
||||||
.where(lt(sessions.expiresAt, new Date().getTime()));
|
.where(lt(sessions.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired sessions:", e);
|
logger.warn("Error clearing expired sessions:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -26,7 +26,7 @@ export async function clearStaleData() {
|
||||||
.delete(newtSessions)
|
.delete(newtSessions)
|
||||||
.where(lt(newtSessions.expiresAt, new Date().getTime()));
|
.where(lt(newtSessions.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired newtSessions:", e);
|
logger.warn("Error clearing expired newtSessions:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -34,7 +34,7 @@ export async function clearStaleData() {
|
||||||
.delete(emailVerificationCodes)
|
.delete(emailVerificationCodes)
|
||||||
.where(lt(emailVerificationCodes.expiresAt, new Date().getTime()));
|
.where(lt(emailVerificationCodes.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired emailVerificationCodes:", e);
|
logger.warn("Error clearing expired emailVerificationCodes:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -42,7 +42,7 @@ export async function clearStaleData() {
|
||||||
.delete(passwordResetTokens)
|
.delete(passwordResetTokens)
|
||||||
.where(lt(passwordResetTokens.expiresAt, new Date().getTime()));
|
.where(lt(passwordResetTokens.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired passwordResetTokens:", e);
|
logger.warn("Error clearing expired passwordResetTokens:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -50,7 +50,7 @@ export async function clearStaleData() {
|
||||||
.delete(userInvites)
|
.delete(userInvites)
|
||||||
.where(lt(userInvites.expiresAt, new Date().getTime()));
|
.where(lt(userInvites.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired userInvites:", e);
|
logger.warn("Error clearing expired userInvites:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -58,7 +58,7 @@ export async function clearStaleData() {
|
||||||
.delete(resourceAccessToken)
|
.delete(resourceAccessToken)
|
||||||
.where(lt(resourceAccessToken.expiresAt, new Date().getTime()));
|
.where(lt(resourceAccessToken.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired resourceAccessToken:", e);
|
logger.warn("Error clearing expired resourceAccessToken:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -66,7 +66,7 @@ export async function clearStaleData() {
|
||||||
.delete(resourceSessions)
|
.delete(resourceSessions)
|
||||||
.where(lt(resourceSessions.expiresAt, new Date().getTime()));
|
.where(lt(resourceSessions.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired resourceSessions:", e);
|
logger.warn("Error clearing expired resourceSessions:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -74,6 +74,6 @@ export async function clearStaleData() {
|
||||||
.delete(resourceOtp)
|
.delete(resourceOtp)
|
||||||
.where(lt(resourceOtp.expiresAt, new Date().getTime()));
|
.where(lt(resourceOtp.expiresAt, new Date().getTime()));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error clearing expired resourceOtp:", e);
|
logger.warn("Error clearing expired resourceOtp:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,9 +40,6 @@ const migrations = [
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
// backup the database
|
|
||||||
backupDb();
|
|
||||||
|
|
||||||
// run the migrations
|
// run the migrations
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
}
|
}
|
||||||
|
@ -127,6 +124,9 @@ async function executeScripts() {
|
||||||
console.log(`Running migration ${migration.version}`);
|
console.log(`Running migration ${migration.version}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Backup the database before running the migration
|
||||||
|
backupDb();
|
||||||
|
|
||||||
await migration.run();
|
await migration.run();
|
||||||
|
|
||||||
// Update version in database
|
// Update version in database
|
||||||
|
|
Loading…
Reference in a new issue