Files
templates/build-scripts/validate-template.ts
Mauricio Siu 53c2ddb2fa New Templates (#586)
* feat(librechat): add LibreChat blueprint with compose, toml, metadata, links and tags

* fix: rename templates to template.toml

* fix(librechat): rename api service to librechat in docker-compose.yml

* Update blueprints/librechat/template.toml

* Update blueprints/librechat/template.toml

* fix(librechat): add version under [config] and remove stray [config.mounts] header

* fix(librechat): remove predefined persistent volume mounts from template.toml

* docs(librechat): add authentication reference link to docker-compose.yml

* feat: add Rote template

- Add Rote deployment template with frontend, backend, and PostgreSQL services
- Configure domain routing for frontend (port 80) and backend (port 3000)
- Set up automatic password generation and environment variables
- Use latest image tag by default
- Add logo and metadata to meta.json

* fix: process meta.json to fix formatting and sorting

* Update GitHub workflows to target 'canary' branch for meta validation

* Update pnpm-lock.yaml to upgrade various dependencies, including '@codemirror/autocomplete', '@radix-ui/react-dialog', and React packages to their latest versions. This includes updates to '@types/react' and '@types/react-dom' for improved compatibility and performance.

* Enhance GitHub workflows: add production deployment configuration and target 'canary' branch for pull requests.

* Refactor GitHub workflow: comment out build preview steps for clarity and future modifications.

* Remove unnecessary blank line in deploy-preview.yml for improved readability.

* Refactor GitHub workflow: uncomment build preview steps for improved deployment process and clarity.

* Update template.toml (#555)

* Update template.toml

* Update template.toml

* Update template.toml

* fix: change VITE_API_BASE to http:// for traefik.me compatibility

* changed image from sknnr/enshrouded-dedicated-server to mornedhels/enshrouded-server for autoupdate and easier config

* Add Openinary Template (#567)

* feat: add Openinary template

* feat: update Openinary configuration to support ALLOWED_ORIGIN and refactor domain variable

* fix: correct DEFAULT_DOMAIN environment variable reference in docker-compose.yml (#562)

* add rustfs template (#568)

* feat: add pull request template for improved contribution guidelines

* fix: update pull request template to clarify issue closing keywords

* feat: add validation scripts and configuration for Docker Compose and template files

- Introduced a GitHub Actions workflow to validate Docker Compose files and template.toml on pull requests.
- Added helper functions for generating random values and processing variables in templates.
- Implemented validation scripts for checking the structure, syntax, and best practices of Docker Compose and template files.
- Created necessary TypeScript types and configuration files for the build scripts.

* Add Passbolt template blueprint to Dokploy templates (#376)

* feat(templates): add Passbolt blueprint for Dokploy
- Add docker-compose.yml defining services for Passbolt and MariaDB
- Create template.toml with configurable domain, email, and database credentials
- Add meta.json with metadata, tags, and link to logo

* fix(meta): sort meta.json entries

* fix: passbolt template had several issues that broke deployment

- env variables were using old array format, changed to new table format
- mariadb healthcheck was broken (wrong command for mariadb 11)
- missing volume mounts for gpg keys, jwt tokens, and database
- setup instructions weren't visible to users, moved to docker-compose
- email config had circular references causing warnings
- tested admin user creation and confirmed working

everything works now, fully tested

* Update blueprints/passbolt/template.toml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat: Add Kokoro TTS FastAPI template (#353) (#403)

* feat: Add Kokoro TTS FastAPI template (#353)  - Add CPU-optimized docker-compose.yml with source build - Add GPU-optimized docker-compose-gpu.yml for NVIDIA support - Add comprehensive template.toml with OpenAI-compatible API docs - Add kokoro-tts.svg logo and meta.json entry - Support streaming audio, timestamps, and multi-language TTS - Resolves #353

* updated the meta.json for the build errors

* removed the docker-compose-gpu.yml file

* Update docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* chore: remove package-lock.json file from the app directory

* chore: update Tolgee to latest version and fix SMTP config typo (#432)

* chore: update Tolgee to latest version and fix SMTP config typo

* Update docker-compose.yml

* Update docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* fix: improve Docker Compose validation workflow to handle subshell issues

- Converted the handling of COMPOSE_FILES from a pipe to an array to ensure error propagation in the parent shell.
- Updated the loop to iterate over the array for better reliability in the validation process.

* refactor: enhance Docker Compose validation workflow to improve error handling

- Replaced the pipe with an array to handle directory names, ensuring that errors within the loop propagate correctly to the parent shell.
- Updated the loop structure for better reliability in processing the directories.

* Feat: Add parseable (#460)

* Add parseable

* Update docker-compose.yml

* Update docker-compose.yml

* Update blueprints/parseable/template.toml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* feat: add ChirpStack LoRaWAN Network Server template (#486)

* feat: add ChirpStack LoRaWAN Network Server template

  Add complete ChirpStack v4 template with:
  - Main ChirpStack server with web UI
  - UDP and Basics Station gateway bridges
  - REST API interface
  - PostgreSQL database with PostGIS extensions
  - Redis cache
  - Mosquitto MQTT broker

Default configuration for EU868 region with secure random credentials. Supports all LoRaWAN frequency bands globally.

* fix(chirpstack): use original configurations from chirpstack-docker repo

Update template.toml to use exact configuration files from the
chirpstack-docker repository instead of simplified versions:

- Use original chirpstack.toml with all 15 enabled regions
- Use original gateway bridge configuration with documentation links
- Use complete Basics Station EU868 config with frequency plans
- Keep original Mosquitto and PostgreSQL initialization scripts

Template size increased from 131 to 219 lines (4.7KB) to include
comprehensive default configurations that match the official setup.

* feat: add all 38 region configuration files

* fix(chirpstack): add volume mounts to expose config files to containers

* fix(chirpstack): remove read-only flag

* fix(chirpstack): correct file paths for configuration mounts in docker-compose and template files

* fix: update volume paths to be on correct directory level

* fix: configure template for dokploy-network with proper DNS resolution

- Add dokploy-network configuration to docker-compose.yml
- Replace environment variable placeholders with actual service hostnames
- Change PostgreSQL DSN from $POSTGRESQL_HOST to postgres
- Change Redis server from $REDIS_HOST to redis
- Replace $MQTT_BROKER_HOST with mosquitto in all 39 region configurations

These changes ensure Docker DNS resolution works correctly by:
- Using dokploy-network (overlay) instead of bridge network
- Using service names directly in TOML config files (TOML doesn't expand env vars)
- Enabling proper service discovery between containers

This resolves DNS resolution failures that caused ChirpStack to fail connecting
to PostgreSQL and MQTT services during deployment.

* fix: add missing network configurations for all services in docker-compose

* feat: add internal services to config.domains for proper network configuration

* Update docker-compose.yml

* fix: enhance domain validation in template validator

- Updated the TemplateValidator to ensure that if the 'host' field is provided, it must be a valid string.
- Added comments to clarify that 'host' is optional for internal services.

* refactor: remove redundant host validation in template validator

- Removed the validation for the 'host' field in the TemplateValidator, as it is optional for internal services and does not require a type check if not provided.

* refactor: remove internal service domain configurations from template

- Eliminated the domain configurations for internal services (Postgres, Redis, Mosquitto) from the template.toml file, streamlining the configuration for better clarity and maintainability.

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* Update section title from 'Suggestions' to 'Requirements'

* Feat : Add MCSManager template support (#521) (#522)

* feat: Add MCSManager template support (#521)

* Update docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat: Add MediaCMS template (#524)

* Feat : Add Quant-Ux template -#173 (#525)

* Feat : Add Quant-Ux template -#173

* Remove extra newline in docker-compose.yml

* Update blueprints/quant-ux/docker-compose.yml

* Update blueprints/quant-ux/docker-compose.yml

* Update blueprints/quant-ux/docker-compose.yml

* Update blueprints/quant-ux/docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* fix(rustdesk): use explicit ports, use port 21118 on hbbs instead of hbbr (#526)

* fix: use explicit ports, use port 21118 on hbbs instead of hbbr

* fix: whitespace character in rustdesk

* feat: Add anytype template (#527)

* add anytype template

* sort

* Update name field for Anytype in meta.json

* Update meta.json

* Update docker-compose.yml

* Update blueprints/anytype/docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* refactor: remove dokploy-network configurations from multiple docker-compose files

- Removed the external dokploy-network configuration from various services' docker-compose.yml files to streamline network management.
- This change simplifies the setup and ensures consistency across blueprints.

* chore: upgrade Infisical from v0.90.1 to v0.135.0 (#529)

* chore: upgrade Infisical from v0.90.1 to v0.135.0

* Update docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* fix: update pull request template link for clarity

- Changed the link in the pull request template from 'general suggestions' to 'general requirements' to better reflect the content and ensure users follow the correct guidelines when creating templates.

* chore: add section for screenshots or videos in pull request template

- Introduced a new section in the pull request template to encourage contributors to include screenshots or videos, enhancing the clarity and context of their submissions.

* Feat : Add MuleSoft ESB Runtime  Template (#498)

* added the mulesoft esb template

* updated the compose and the meta.json

* feat(mulesoft-esb): update image and add dynamic env configuration  - Updated image to hari1367709/mule-esb:latest - Added dynamic HTTP_PORT for runtime port configuration - Added MULE_VERSION environment variable for Mule ESB version selection

* updated the meta.json to use the version as latest

* added a comment line to the template file

* updated the mule runtime image

* fix(mulesoft-esb): update ports configuration to follow guidelines

* updated the port to use the env(HTTP_PORT)

* Update docker-compose.yml

* Update docker-compose.yml

* Update blueprints/mulesoft-esb/docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat(blueprint): update trmnl-byos-laravel template (#533)

* feat(blueprint): update trmnl-byos-laravel template

* Update docker-compose.yml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat(blueprint): peerdb template (#579)

* feat(blueprint): initial attempt at peerdb template

* fix: entrypoint and healthcheck

* fix: entrypoint

* fix: temporarily remove network

* fix: temporal port

* chore: remove 36987 for minio

* fix: remove peerdb 9900 port exposure

* fix: port for console

* fix: minio env fix

* fix: expose peerdb and minio to dokploy network

* fix(peerdb): add defaults

* fix: remove extra hosts

* fix: remove network entries

* fix: use consistent environment variables

* feat: add Bluesky PDS template (#542)

* feat: Bluesky PDS template

* chore: add bluesky pds svg

* chore: metadata for bluesky pds

* yaml > yml

* pnpm lock

* fix: correct rotation key config

* fix volumes

* fix: volumes in the pds compose

* define volumes in compose

* fix: 32 bit rotation key

* create pds.env correctly

* some extra fixes

* more extra fixes

* a blank line

* update pnpm lock

* Add dokploy-prom-monitoring-extension template with comprehensive tests and documentation (#548)

* Add dokploy-prom-monitoring-extension template with comprehensive tests and documentation

* Fix METRICS_CONFIG environment variable: use single-line JSON format

* Fix template.toml: use correct [config.env] syntax for environment variables

* Fix docker-compose.yml: add env_file reference to load environment variables

* Delete blueprints/dokploy-prom-monitoring-extension/README.md

* Delete test-dokploy-prom-monitoring-extension.sh

---------

Co-authored-by: Sanjeevi Subramani <ssanjeevi.ss@gmail.com>
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat: improve RustDesk template configuration (#571)

* feat: improve RustDesk template configuration

- Add comprehensive environment variables for RustDesk server
- Add RELAY_HOST, API_SERVER, ID_SERVER, and ENCRYPTION_KEY variables
- Follow Dokploy best practices (no container_name, proper port format)
- Use restart: unless-stopped policy
- Add encryption key generation with password helper

* fix: use explicit port mapping for RustDesk services

RustDesk requires explicit port bindings (host:container format) to function properly. The service uses specific ports for:
- 21115-21116 (TCP/UDP): hbbs service for ID and NAT traversal
- 21117-21119 (TCP): hbbr relay service

Without explicit port mapping, RustDesk clients cannot establish connections to the server.

This is an exception to Dokploy's general port guidelines due to RustDesk's specific networking requirements.

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* feat: add Mumble voice chat server template (#572)

* feat: add Mumble voice chat server template

- Add Mumble VoIP server blueprint with docker-compose.yml
- Configure environment variables for superuser password, welcome text, and max users
- Add template.toml with auto-generated secure password
- Follow Dokploy best practices (no container_name, proper port format)
- Add Mumble metadata to meta.json with proper tags
- Support for TCP and UDP on port 64738

* Update template.toml

* fix: correct JSON formatting in meta.json for Mumble template entry

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* fix: update WireGuard Easy template for proper functionality (#573)

* fix: update WireGuard Easy template for proper functionality

- Changed to named volume (etc_wireguard) instead of host path mount
- Added explicit port mappings (51820:51820/udp, 51821:51821/tcp) required for WireGuard
- Updated environment variables to use correct WG_HOST and PASSWORD format
- Added all required WireGuard environment variables:
  - WG_PORT, PORT, WG_MTU, WG_DEFAULT_DNS, WG_ALLOWED_IPS
  - WG_POST_UP/WG_POST_DOWN for iptables rules
- Added NET_RAW capability for proper network operations
- Simplified template.toml to use WIREGUARD_HOST and WIREGUARD_PASSWORD
- Removed explicit networks config to enable Dokploy's isolated deployment
- Template now works with Dokploy's automatic network isolation

This configuration has been tested and confirmed working with isolated deployment enabled.

* Update template.toml

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>

* add: restart policy to MinIO service (#576)

restart: unless-stopped is a Docker restart policy that automatically restarts a container if it stops due to an error or Docker daemon restart

---------

Co-authored-by: Sunil Shrestha <sunil.shrestha@tekkon.com.np>
Co-authored-by: Rabithua <rabithua@gmail.com>
Co-authored-by: Mauricio Siu <siumauricio@hotmail.com>
Co-authored-by: Scan <103391616+scanash00@users.noreply.github.com>
Co-authored-by: Crackvignoule <kiki.kalagan@gmail.com>
Co-authored-by: florianheysen <39408021+florianheysen@users.noreply.github.com>
Co-authored-by: Thiago MadPin <madpin@gmail.com>
Co-authored-by: BlinkStrike <18644035+BlinkStrike@users.noreply.github.com>
Co-authored-by: M Jupri Amin <127651222+Juupeee@users.noreply.github.com>
Co-authored-by: Harikrishnan Dhanasekaran <harikrishnan@mulecraft.in>
Co-authored-by: Kamil Dzieniszewski <kamil.dzieniszewski@gmail.com>
Co-authored-by: Nick Anderson <nbrookie@gmail.com>
Co-authored-by: lefolalan <alan.lefol@omirion.com>
Co-authored-by: Chris <31969757+ChrisvanChip@users.noreply.github.com>
Co-authored-by: kipavy <88386090+kipavy@users.noreply.github.com>
Co-authored-by: Benjamin Nussbaum <bnussbau@users.noreply.github.com>
Co-authored-by: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com>
Co-authored-by: Vidhya LKG for IT <24915474+VidhyaSanjeevi@users.noreply.github.com>
Co-authored-by: Sanjeevi Subramani <ssanjeevi.ss@gmail.com>
Co-authored-by: Muzaffer Kadir YILMAZ <34358176+muzafferkadir@users.noreply.github.com>
Co-authored-by: Jemg <murksopps@gmail.com>
2025-12-14 23:40:25 -06:00

623 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
/**
* Validation script for template.toml and docker-compose.yml files
* Validates structure, syntax, and consistency between files
*/
import * as fs from "fs";
import * as path from "path";
import { parse } from "toml";
import * as yaml from "yaml";
import type { ComposeSpecification } from "./type";
import { processVariables, processValue, type Schema } from "./helpers";
interface TemplateValidatorOptions {
templateDir?: string | null;
composeServices?: string[] | null;
verbose?: boolean;
exitOnError?: boolean;
}
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
interface DomainConfig {
serviceName?: string;
port?: number | string;
host?: string;
path?: string;
}
interface MountConfig {
filePath?: string;
content?: string;
}
interface TemplateData {
variables?: Record<string, string>;
config?: {
domains?: DomainConfig[];
env?: string[] | Record<string, string | boolean | number> | Array<string | Record<string, string | boolean | number>>;
mounts?: MountConfig[];
};
}
type LogLevel = "info" | "success" | "warning" | "error" | "debug";
class TemplateValidator {
private options: Required<TemplateValidatorOptions>;
private errors: string[] = [];
private warnings: string[] = [];
constructor(options: TemplateValidatorOptions = {}) {
this.options = {
templateDir: options.templateDir || null,
composeServices: options.composeServices || null,
verbose: options.verbose || false,
exitOnError: options.exitOnError !== false,
...options,
};
}
private log(message: string, level: LogLevel = "info"): void {
if (!this.options.verbose && level === "debug") return;
const prefix: Record<LogLevel, string> = {
info: "🔍",
success: "✅",
warning: "⚠️",
error: "❌",
debug: "🔍",
};
console.log(`${prefix[level] || ""} ${message}`);
}
private error(message: string): void {
this.errors.push(message);
this.log(message, "error");
}
private warning(message: string): void {
this.warnings.push(message);
this.log(message, "warning");
}
/**
* Validate helper syntax (based on Dokploy's processValue function)
*/
private validateHelper(helper: string, context: string = ""): void {
const validHelpers = [
"domain",
"base64",
"password",
"hash",
"uuid",
"timestamp",
"timestampms",
"timestamps",
"randomPort",
"jwt",
"username",
"email",
];
// Check if it's a helper with parameters
if (helper.includes(":")) {
const [helperName, ...params] = helper.split(":");
// Validate helper name
if (!validHelpers.includes(helperName)) {
// Might be a variable reference, which is valid
return;
}
// Validate parameter formats
if (helperName === "base64" || helperName === "password" || helperName === "hash") {
// Format: helper:number
const param = params[0];
if (param && isNaN(parseInt(param, 10))) {
this.warning(
`${context}: helper '${helper}' has invalid parameter (should be a number)`
);
}
} else if (helperName === "timestampms" || helperName === "timestamps") {
// Format: timestampms:datetime or timestamps:datetime
const datetime = params.join(":"); // Rejoin in case datetime has colons
if (datetime) {
// Try to parse as date
const date = new Date(datetime);
if (isNaN(date.getTime())) {
this.warning(
`${context}: helper '${helper}' has invalid datetime format`
);
}
}
} else if (helperName === "jwt") {
// Format: jwt:secret or jwt:secret:payload or jwt:length
if (params.length > 0) {
const firstParam = params[0];
// If it's a number, it's jwt:length (deprecated but valid)
if (!isNaN(parseInt(firstParam, 10))) {
// Valid: jwt:32
return;
}
// Otherwise it's jwt:secret or jwt:secret:payload
// Both are valid
}
}
} else {
// Simple helper without parameters
if (!validHelpers.includes(helper)) {
// Might be a variable reference, which is valid
return;
}
}
}
/**
* Parse docker-compose.yml and extract service names
*/
private parseComposeServices(composePath: string): string[] {
try {
if (!fs.existsSync(composePath)) {
this.warning(`docker-compose.yml not found at ${composePath}`);
return [];
}
const content = fs.readFileSync(composePath, "utf8");
const compose = yaml.parse(content) as ComposeSpecification;
if (!compose || typeof compose !== "object") {
this.error(`Invalid docker-compose.yml structure at ${composePath}`);
return [];
}
// Extract service names using the official ComposeSpecification type
const services = compose.services || {};
const serviceNames = Object.keys(services);
if (serviceNames.length === 0) {
this.warning(`No services found in docker-compose.yml at ${composePath}`);
}
return serviceNames;
} catch (error: any) {
this.error(
`Failed to parse docker-compose.yml at ${composePath}: ${error.message}`
);
return [];
}
}
/**
* Validate template.toml structure
*/
private validateTemplate(tomlPath: string, composeServices: string[] | null = null): boolean {
try {
if (!fs.existsSync(tomlPath)) {
this.error(`template.toml not found at ${tomlPath}`);
return false;
}
// Parse TOML
let data: TemplateData;
try {
const content = fs.readFileSync(tomlPath, "utf8");
data = parse(content) as TemplateData;
} catch (parseError: any) {
this.error(
`Invalid TOML syntax in ${tomlPath}: ${parseError.message}`
);
return false;
}
// Validate [config] section exists
if (!data.config) {
this.error("Missing [config] section in template.toml");
return false;
}
// Validate domains
if (data.config.domains) {
if (!Array.isArray(data.config.domains)) {
this.error("config.domains must be an array");
return false;
}
data.config.domains.forEach((domain, index) => {
// Required fields
if (!domain.serviceName) {
this.error(`domain[${index}]: Missing required field 'serviceName'`);
}
if (domain.port === undefined || domain.port === null) {
this.error(`domain[${index}]: Missing required field 'port'`);
}
// Validate serviceName matches docker-compose.yml services
if (domain.serviceName && composeServices && composeServices.length > 0) {
if (!composeServices.includes(domain.serviceName)) {
this.error(
`domain[${index}]: serviceName '${domain.serviceName}' not found in docker-compose.yml services. Available services: ${composeServices.join(", ")}`
);
}
}
// Validate port is a number
if (domain.port !== undefined && domain.port !== null) {
const port = typeof domain.port === "string"
? parseInt(domain.port.replace(/_/g, ""), 10)
: domain.port;
if (isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535) {
this.warning(
`domain[${index}]: port '${domain.port}' may be invalid (should be 1-65535)`
);
}
}
// Validate host format (should contain ${} for variable substitution)
if (domain.host && typeof domain.host === "string") {
if (!domain.host.includes("${")) {
this.warning(
`domain[${index}]: host '${domain.host}' doesn't use variable syntax (e.g., \${main_domain} or \${domain})`
);
} else {
// Validate helpers in host
const helperPattern = /\${([^}]+)}/g;
let match: RegExpExecArray | null;
while ((match = helperPattern.exec(domain.host)) !== null) {
this.validateHelper(match[1], `domain[${index}].host`);
}
}
}
});
} else {
this.warning("No domains configured in template.toml");
}
// Validate env - can be array or object (as per Dokploy's processEnvVars)
if (data.config.env !== undefined) {
if (Array.isArray(data.config.env)) {
// Array format: ["KEY=VALUE", ...]
data.config.env.forEach((env, index) => {
if (typeof env === "string") {
if (!env.includes("=")) {
this.warning(
`config.env[${index}]: '${env}' doesn't follow KEY=VALUE format`
);
}
} else if (typeof env === "object" && env !== null) {
// Object in array is also valid: [{"KEY": "VALUE"}, ...]
const keys = Object.keys(env);
if (keys.length === 0) {
this.warning(`config.env[${index}]: empty object`);
}
} else if (typeof env !== "boolean" && typeof env !== "number") {
this.error(
`config.env[${index}]: must be a string, object, boolean, or number`
);
}
});
} else if (typeof data.config.env === "object" && data.config.env !== null) {
// Object format: { KEY: "VALUE", ... }
// This is valid - Dokploy handles both formats
const envKeys = Object.keys(data.config.env);
if (envKeys.length === 0) {
this.warning("config.env is an empty object");
}
} else {
this.error(
"config.env must be an array or an object (as per Dokploy's processEnvVars)"
);
}
}
// Validate mounts if present
if (data.config.mounts) {
if (!Array.isArray(data.config.mounts)) {
this.error("config.mounts must be an array");
} else {
data.config.mounts.forEach((mount, index) => {
if (!mount.filePath) {
this.error(`config.mounts[${index}]: Missing required field 'filePath'`);
} else if (typeof mount.filePath !== "string") {
this.error(`config.mounts[${index}]: filePath must be a string`);
}
if (mount.content === undefined) {
this.error(`config.mounts[${index}]: Missing required field 'content'`);
} else if (typeof mount.content !== "string") {
this.error(`config.mounts[${index}]: content must be a string`);
}
});
}
}
// Validate variables if present
if (data.variables) {
if (typeof data.variables !== "object" || Array.isArray(data.variables)) {
this.error("variables must be an object");
} else {
// Validate variable values and helpers
Object.entries(data.variables).forEach(([key, value]) => {
if (typeof value !== "string") {
this.error(`variables.${key}: must be a string`);
return;
}
// Validate helpers in variable values
const helperPattern = /\${([^}]+)}/g;
let match: RegExpExecArray | null;
while ((match = helperPattern.exec(value)) !== null) {
const helper = match[1];
this.validateHelper(helper, `variables.${key}`);
}
});
// Try to process variables to ensure they resolve correctly
try {
const schema: Schema = {};
const processedVars = processVariables(data.variables, schema);
// Check if any variables failed to resolve (still contain ${})
Object.entries(processedVars).forEach(([key, value]) => {
if (typeof value === "string" && value.includes("${")) {
// Check if it's a valid variable reference or an error
const unresolved = value.match(/\${([^}]+)}/g);
if (unresolved) {
unresolved.forEach((unresolvedVar) => {
const varName = unresolvedVar.slice(2, -1);
// Check if it's a reference to another variable that exists
if (!data.variables![varName] && !varName.includes(":")) {
this.warning(
`variables.${key}: contains unresolved variable reference '${unresolvedVar}'`
);
}
});
}
}
});
// Validate that domains can be processed with resolved variables
if (data.config.domains) {
data.config.domains.forEach((domain, index) => {
if (domain.host && typeof domain.host === "string") {
try {
const processedHost = processValue(domain.host, processedVars, schema);
if (processedHost.includes("${")) {
this.warning(
`domain[${index}].host: could not fully resolve all variables. Result: ${processedHost}`
);
}
} catch (e: any) {
this.warning(
`domain[${index}].host: error processing host value: ${e.message}`
);
}
}
});
}
// Validate that env vars can be processed
if (data.config.env) {
if (Array.isArray(data.config.env)) {
data.config.env.forEach((env, index) => {
if (typeof env === "string") {
try {
const processed = processValue(env, processedVars, schema);
if (processed.includes("${")) {
this.warning(
`config.env[${index}]: could not fully resolve all variables`
);
}
} catch (e: any) {
this.warning(
`config.env[${index}]: error processing env value: ${e.message}`
);
}
}
});
} else if (typeof data.config.env === "object") {
Object.entries(data.config.env).forEach(([key, value]) => {
if (typeof value === "string") {
try {
const processed = processValue(value, processedVars, schema);
if (processed.includes("${")) {
this.warning(
`config.env.${key}: could not fully resolve all variables`
);
}
} catch (e: any) {
this.warning(
`config.env.${key}: error processing env value: ${e.message}`
);
}
}
});
}
}
// Validate that mounts can be processed
if (data.config.mounts) {
data.config.mounts.forEach((mount, index) => {
if (mount.filePath && typeof mount.filePath === "string") {
try {
const processed = processValue(mount.filePath, processedVars, schema);
if (processed.includes("${")) {
this.warning(
`config.mounts[${index}].filePath: could not fully resolve all variables`
);
}
} catch (e: any) {
this.warning(
`config.mounts[${index}].filePath: error processing filePath: ${e.message}`
);
}
}
if (mount.content && typeof mount.content === "string") {
try {
const processed = processValue(mount.content, processedVars, schema);
if (processed.includes("${")) {
this.warning(
`config.mounts[${index}].content: could not fully resolve all variables`
);
}
} catch (e: any) {
this.warning(
`config.mounts[${index}].content: error processing content: ${e.message}`
);
}
}
});
}
if (this.options.verbose) {
this.log("✅ Variables processed successfully", "success");
this.log(`📋 Processed ${Object.keys(processedVars).length} variables`, "debug");
}
} catch (e: any) {
this.error(`Failed to process variables: ${e.message}`);
}
}
}
return this.errors.length === 0;
} catch (error: any) {
this.error(`Error validating template.toml: ${error.message}`);
return false;
}
}
/**
* Validate a template directory
*/
private validateTemplateDir(templateDir: string): ValidationResult {
// Resolver rutas absolutas o relativas desde la raíz del proyecto
const resolvedDir = path.isAbsolute(templateDir)
? templateDir
: path.resolve(process.cwd(), templateDir);
const templatePath = path.join(resolvedDir, "template.toml");
const composePath = path.join(resolvedDir, "docker-compose.yml");
this.log(`Validating template: ${path.basename(resolvedDir)}`);
// Parse compose services first
const composeServices = this.parseComposeServices(composePath);
// Validate template.toml
const isValid = this.validateTemplate(templatePath, composeServices);
// Show summary
if (isValid && this.errors.length === 0) {
this.log("Template structure is valid", "success");
// Show domains info
try {
const content = fs.readFileSync(templatePath, "utf8");
const data = parse(content) as TemplateData;
if (data.config && data.config.domains) {
this.log("📋 Domains configured:");
data.config.domains.forEach((domain) => {
const service = domain.serviceName || "N/A";
const port = domain.port !== undefined ? domain.port : "N/A";
const host = domain.host || "N/A";
this.log(` - Service: ${service}, Port: ${port}, Host: ${host}`);
});
}
} catch (e) {
// Ignore errors in summary
}
}
return {
valid: isValid && this.errors.length === 0,
errors: this.errors,
warnings: this.warnings,
};
}
/**
* Main validation method
*/
validate(): ValidationResult {
if (!this.options.templateDir) {
this.error("templateDir option is required");
if (this.options.exitOnError) {
process.exit(1);
}
return { valid: false, errors: this.errors, warnings: this.warnings };
}
const result = this.validateTemplateDir(this.options.templateDir!);
if (!result.valid && this.options.exitOnError) {
process.exit(1);
}
return result;
}
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const options: TemplateValidatorOptions = {};
let templateDir: string | null = null;
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--dir":
case "-d":
templateDir = args[++i];
break;
case "--verbose":
case "-v":
options.verbose = true;
break;
case "--help":
case "-h":
console.log(`
Usage: tsx validate-template.ts [options]
Options:
-d, --dir <path> Template directory path (required)
-v, --verbose Verbose output
-h, --help Show this help message
Examples:
tsx validate-template.ts --dir blueprints/grafana
tsx validate-template.ts -d blueprints/grafana --verbose
`);
process.exit(0);
break;
}
}
if (!templateDir) {
console.error("❌ Error: --dir option is required");
console.error("Use --help for usage information");
process.exit(1);
}
const validator = new TemplateValidator({
templateDir,
...options,
});
const result = validator.validate();
// Exit with appropriate code
process.exit(result.valid ? 0 : 1);
}
export default TemplateValidator;