2679c9d66e
v1.27.6-debug does not exist on gcr.io; proxy-gcr pulls are flaky on kubelet. Seed and reference abrban/kaniko-executor:v1.23.2 instead. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
9.9 KiB
TypeScript
236 lines
9.9 KiB
TypeScript
// FRONTEND_URL may hold a comma-separated list of origins (used for CORS).
|
|
// Domain/host resolution must only ever look at a single URL, otherwise
|
|
// `new URL('https://a.com,https://b.com')` yields the garbage hostname
|
|
// "a.com,https" which then leaks into ingress hosts and gets rejected by k8s.
|
|
function firstFrontendUrl(): string | undefined {
|
|
const first = (process.env.FRONTEND_URL || '').split(',')[0]?.trim();
|
|
return first || undefined;
|
|
}
|
|
|
|
function resolvePlatformDomainFromEnv(): string {
|
|
const frontendUrl = firstFrontendUrl();
|
|
if (frontendUrl) {
|
|
try {
|
|
const url = new URL(frontendUrl);
|
|
if (url.hostname && !/^\d+\.\d+\.\d+\.\d+$/.test(url.hostname)) {
|
|
return url.hostname;
|
|
}
|
|
} catch {
|
|
// ignore parse errors and fall back to PLATFORM_DOMAIN
|
|
}
|
|
}
|
|
return process.env.PLATFORM_DOMAIN || 'apps.cloudhost.local';
|
|
}
|
|
|
|
function stripLeadingSubdomain(hostname: string): string {
|
|
const h = (hostname || '').toLowerCase();
|
|
if (!h) return h;
|
|
// If it looks like an IP (IPv4 or IPv6), don't try to strip labels.
|
|
if (h.includes(':') || /^\d+\.\d+\.\d+\.\d+$/.test(h)) {
|
|
return h;
|
|
}
|
|
const parts = h.split('.').filter(Boolean);
|
|
if (parts.length >= 3) {
|
|
return parts.slice(1).join('.');
|
|
}
|
|
return h;
|
|
}
|
|
|
|
function resolvePreviewRootDomainFromEnv(): string {
|
|
// Explicit override takes priority — the base domain used for preview URLs
|
|
// (e.g. PREVIEW_BASE_DOMAIN=3fase.ir) must be configurable, never hardcoded.
|
|
const explicit = process.env.PREVIEW_BASE_DOMAIN;
|
|
if (explicit && explicit.trim()) {
|
|
return explicit.trim().toLowerCase();
|
|
}
|
|
|
|
const frontendUrl = firstFrontendUrl();
|
|
if (frontendUrl) {
|
|
try {
|
|
const url = new URL(frontendUrl);
|
|
if (url.hostname) {
|
|
return stripLeadingSubdomain(url.hostname);
|
|
}
|
|
} catch {
|
|
// ignore and fall back
|
|
}
|
|
}
|
|
|
|
// Fallback: strip from PLATFORM_DOMAIN if it's already a subdomain host.
|
|
return stripLeadingSubdomain(process.env.PLATFORM_DOMAIN || 'apps.cloudhost.local');
|
|
}
|
|
|
|
export default () => ({
|
|
nodeEnv: process.env.NODE_ENV || 'development',
|
|
port: parseInt(process.env.PORT || '4000', 10),
|
|
|
|
database: {
|
|
host: process.env.DB_HOST || 'localhost',
|
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
|
username: process.env.DB_USERNAME || 'cloudhost',
|
|
password: process.env.DB_PASSWORD || 'cloudhost_secret',
|
|
name: process.env.DB_DATABASE || 'cloudhost',
|
|
},
|
|
|
|
jwt: {
|
|
secret: process.env.JWT_SECRET || 'default-jwt-secret',
|
|
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
|
|
refreshSecret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret',
|
|
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
|
},
|
|
|
|
redis: {
|
|
host: process.env.REDIS_HOST || 'localhost',
|
|
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
password: process.env.REDIS_PASSWORD || undefined,
|
|
},
|
|
|
|
cluster: {
|
|
/** AES-256-GCM key for encrypting stored kubeconfigs. Required in production. */
|
|
kubeconfigKey: process.env.CLUSTER_KUBECONFIG_KEY || '',
|
|
},
|
|
|
|
// OTP SMS. Provider selectable via SMS_PROVIDER ('mizbansms' | 'kavenegar').
|
|
sms: {
|
|
provider: (process.env.SMS_PROVIDER || 'mizbansms').trim().toLowerCase(),
|
|
|
|
// Kavenegar (verify/lookup template API) — kept for fallback.
|
|
kavenegarApiKey: process.env.KAVENEGAR_API_KEY || '',
|
|
kavenegarOtpTemplate: process.env.KAVENEGAR_OTP_TEMPLATE || '',
|
|
|
|
// MizbanSMS (services.mizbansms.com) — sends full message text over a
|
|
// dedicated line, so the OTP wording is composed here, not from a template.
|
|
mizban: {
|
|
baseUrl: process.env.MIZBANSMS_BASE_URL || 'http://services.mizbansms.com',
|
|
username: process.env.MIZBANSMS_USERNAME || '',
|
|
password: process.env.MIZBANSMS_PASSWORD || '',
|
|
from: process.env.MIZBANSMS_FROM || '5000467254',
|
|
api: parseInt(process.env.MIZBANSMS_API || '2016', 10),
|
|
userType: parseInt(process.env.MIZBANSMS_USERTYPE || '2', 10),
|
|
// Per-message-kind wording. `{code}` (or `{{CODE}}`) is replaced with the
|
|
// 6-digit OTP. \n is allowed for multi-line messages.
|
|
templates: {
|
|
register:
|
|
process.env.MIZBANSMS_OTP_TEMPLATE_REGISTER ||
|
|
'به ابربان خوش آمدید.\nکد تأیید شما: {code}',
|
|
login:
|
|
process.env.MIZBANSMS_OTP_TEMPLATE_LOGIN ||
|
|
'رمز یکبار مصرف ابربان:\n{code}',
|
|
changePhone:
|
|
process.env.MIZBANSMS_OTP_TEMPLATE_CHANGE_PHONE ||
|
|
'کد تأیید شماره جدید ابربان:\n{code}',
|
|
},
|
|
},
|
|
},
|
|
|
|
registry: {
|
|
/** In-cluster registry — Kaniko push and app image pull (same host). */
|
|
url: process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000',
|
|
pullUrl: process.env.REGISTRY_PULL_URL || process.env.REGISTRY_URL || 'registry.cloudhost-builds.svc.cluster.local:5000',
|
|
username: process.env.REGISTRY_USERNAME || 'admin',
|
|
password: process.env.REGISTRY_PASSWORD || '',
|
|
},
|
|
|
|
build: {
|
|
namespace: process.env.BUILD_NAMESPACE || 'cloudhost-builds',
|
|
serviceAccount: process.env.BUILD_SERVICE_ACCOUNT || 'kaniko-builder',
|
|
/**
|
|
* Registry prefix for Docker Hub base images used in generated Dockerfiles
|
|
* and managed-service charts (e.g. `node:20-alpine` →
|
|
* `registry.abrban.com/proxy-dockerhub/library/node:20-alpine`).
|
|
*/
|
|
baseImageRegistry: (process.env.BASE_IMAGE_REGISTRY || 'registry.abrban.com/proxy-dockerhub/library')
|
|
.trim()
|
|
.replace(/\/+$/, ''),
|
|
/** Full image refs for Kaniko jobs — override via Helm values or env. */
|
|
images: {
|
|
kaniko:
|
|
process.env.KANIKO_IMAGE ||
|
|
'registry.abrban.com/abrban/kaniko-executor:v1.23.2',
|
|
alpine: (process.env.BUILD_ALPINE_IMAGE || 'registry.abrban.com/abrban/alpine:3.19').trim(),
|
|
alpineGit: (process.env.BUILD_ALPINE_GIT_IMAGE || 'registry.abrban.com/abrban/alpine-git:2.43.0').trim(),
|
|
},
|
|
/**
|
|
* Secret name with HTTP_PROXY / HTTPS_PROXY / NO_PROXY for build pods
|
|
* (Kaniko RUN steps: npm, apk, composer, pip; init containers: apk, git clone).
|
|
* Empty = disabled (clusters with direct egress).
|
|
*/
|
|
egressProxySecret: (process.env.BUILD_EGRESS_PROXY_SECRET || '').trim(),
|
|
/** Kaniko build container resources — tune for large images. */
|
|
kaniko: {
|
|
cpuRequest: process.env.KANIKO_CPU_REQUEST || '500m',
|
|
cpuLimit: process.env.KANIKO_CPU_LIMIT || '2',
|
|
memoryRequest: process.env.KANIKO_MEMORY_REQUEST || '1Gi',
|
|
memoryLimit: process.env.KANIKO_MEMORY_LIMIT || '4Gi',
|
|
},
|
|
},
|
|
|
|
elasticsearch: {
|
|
/** API host for log search. In-cluster: cluster DNS. Local dev: loopback + auto port-forward. */
|
|
host:
|
|
process.env.ELASTICSEARCH_HOST ||
|
|
(process.env.KUBERNETES_SERVICE_HOST
|
|
? 'elasticsearch.logging.svc.cluster.local'
|
|
: '127.0.0.1'),
|
|
port: parseInt(process.env.ELASTICSEARCH_PORT || '9200', 10),
|
|
/** In development with loopback host, start kubectl port-forward on API boot (set false to manage manually). */
|
|
autoPortForward: process.env.ELASTICSEARCH_AUTO_PORT_FORWARD ?? 'true',
|
|
password: process.env.ELASTIC_PASSWORD || 'CloudHost2024!Secure',
|
|
fluentbitPassword: process.env.FLUENTBIT_PASSWORD || 'FluentBit2024!Writer',
|
|
kibanaPassword: process.env.KIBANA_SYSTEM_PASSWORD || 'Kibana2024!System',
|
|
/** Override when cluster nodes cannot reach docker.elastic.co (mirror to local registry). */
|
|
images: {
|
|
elasticsearch:
|
|
process.env.LOGGING_ELASTICSEARCH_IMAGE ||
|
|
'docker.elastic.co/elasticsearch/elasticsearch:8.12.0',
|
|
kibana: process.env.LOGGING_KIBANA_IMAGE || 'docker.elastic.co/kibana/kibana:8.12.0',
|
|
busybox: process.env.LOGGING_BUSYBOX_IMAGE || 'busybox:1.36',
|
|
curl: process.env.LOGGING_CURL_IMAGE || 'curlimages/curl:8.5.0',
|
|
},
|
|
},
|
|
|
|
sourceStorage: {
|
|
endpoint: process.env.SOURCE_STORAGE_ENDPOINT,
|
|
region: process.env.SOURCE_STORAGE_REGION || 'us-east-1',
|
|
bucket: process.env.SOURCE_STORAGE_BUCKET,
|
|
accessKey: process.env.SOURCE_STORAGE_ACCESS_KEY,
|
|
secretKey: process.env.SOURCE_STORAGE_SECRET_KEY,
|
|
},
|
|
|
|
platform: {
|
|
domain: resolvePlatformDomainFromEnv(),
|
|
previewRootDomain: resolvePreviewRootDomainFromEnv(),
|
|
/**
|
|
* Ingress controller class used for app Ingress objects and the ACME
|
|
* HTTP-01 solver. k3s ships Traefik by default, so we default to "traefik".
|
|
* Set INGRESS_CLASS=nginx for clusters running ingress-nginx instead.
|
|
*/
|
|
ingressClass: (process.env.INGRESS_CLASS || 'traefik').trim().toLowerCase(),
|
|
uploadDir: process.env.UPLOAD_DIR || './uploads',
|
|
/** StorageClass for new PVCs; must support allowVolumeExpansion for disk resize */
|
|
storageClass: process.env.PLATFORM_STORAGE_CLASS || 'cloudhost-expandable',
|
|
/** Install cloudhost-expandable StorageClass via Helm on each app deploy */
|
|
createStorageClass: process.env.PLATFORM_CREATE_STORAGE_CLASS !== 'false',
|
|
storageProvisioner: process.env.PLATFORM_STORAGE_PROVISIONER || 'rancher.io/local-path',
|
|
},
|
|
|
|
// Lifecycle defaults (can be overridden via PlatformSettings entity by admin)
|
|
lifecycle: {
|
|
// How long after plan expires to suspend the app (scale to 0)
|
|
hourly: {
|
|
suspendAfterMs: parseInt(process.env.LIFECYCLE_HOURLY_SUSPEND_MS || String(0), 10), // immediately on expiry
|
|
deleteAfterMs: parseInt(process.env.LIFECYCLE_HOURLY_DELETE_MS || String(24 * 3600 * 1000), 10), // 24h after suspend
|
|
},
|
|
monthly: {
|
|
suspendAfterMs: parseInt(process.env.LIFECYCLE_MONTHLY_SUSPEND_MS || String(0), 10),
|
|
deleteAfterMs: parseInt(process.env.LIFECYCLE_MONTHLY_DELETE_MS || String(3 * 24 * 3600 * 1000), 10), // 3 days
|
|
},
|
|
yearly: {
|
|
suspendAfterMs: parseInt(process.env.LIFECYCLE_YEARLY_SUSPEND_MS || String(0), 10),
|
|
deleteAfterMs: parseInt(process.env.LIFECYCLE_YEARLY_DELETE_MS || String(7 * 24 * 3600 * 1000), 10), // 7 days
|
|
},
|
|
// Cron interval for the lifecycle scanner
|
|
scanIntervalMs: parseInt(process.env.LIFECYCLE_SCAN_INTERVAL_MS || String(60_000), 10), // 1 min
|
|
},
|
|
});
|