From f4b2a624692bf03ead0fc2e3f47deb79e609fa65 Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Sun, 19 Jan 2025 14:53:26 +0100 Subject: [PATCH 1/4] Make the reverse_proxy configurable --- ...reate_domain.ts => create_caddy_domain.ts} | 15 ++----- src/blocks/create_directory.ts | 17 +++++--- src/blocks/index.ts | 2 +- src/blocks/install_caddy.ts | 17 ++++---- src/files.ts | 25 ++++++++++++ src/files/server_caddyfile | 11 ++++++ src/files/service_caddyfile | 5 +++ src/reverse_proxy/caddy.ts | 36 +++++++++++++++++ src/reverse_proxy/index.ts | 1 + src/server.ts | 8 +++- src/services/app/git.ts | 12 ++++-- src/types.ts | 19 +++++++++ tasks.todo | 39 ++++++------------- tests/app-laravel-mysql-custom-docker.test.ts | 1 + 14 files changed, 149 insertions(+), 59 deletions(-) rename src/blocks/{create_domain.ts => create_caddy_domain.ts} (59%) create mode 100644 src/files.ts create mode 100644 src/files/server_caddyfile create mode 100644 src/files/service_caddyfile create mode 100644 src/reverse_proxy/caddy.ts create mode 100644 src/reverse_proxy/index.ts diff --git a/src/blocks/create_domain.ts b/src/blocks/create_caddy_domain.ts similarity index 59% rename from src/blocks/create_domain.ts rename to src/blocks/create_caddy_domain.ts index f88b445..ac26ff1 100644 --- a/src/blocks/create_domain.ts +++ b/src/blocks/create_caddy_domain.ts @@ -4,25 +4,18 @@ import { block } from './block.js' type Config = { domain: string - ports_var: string caddyfile_path: string + caddyfile_content: string } -const reverse_proxy = (x: Config) => - `${x.domain} { - reverse_proxy {{ ${x.ports_var} | map('regex_replace', '^', '127.0.0.1:') | join(' ') }} { - lb_policy client_ip_hash - } -}` - -export function create_domain(config: Config): Block { - return block(`Configure domain: ${config.domain}`, {}, [ +export function create_caddy_domain(config: Config): Block { + return block(`Configure Caddy domain: ${config.domain}`, {}, [ builtin.lineinfile( `Ensure ${config.domain} is in /etc/hosts`, { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present' }, { become: true }, ), - builtin.copy(`Create Caddyfile for ${config.domain}`, { dest: config.caddyfile_path, content: reverse_proxy(config) }, { register: 'caddyfile' }), + builtin.copy(`Create Caddyfile for ${config.domain}`, { dest: config.caddyfile_path, content: config.caddyfile_content }, { register: 'caddyfile' }), builtin.command(`Reload caddy`, { cmd: `sudo systemctl reload caddy` }, { become: true, when: 'caddyfile.changed' }), ]).get() } diff --git a/src/blocks/create_directory.ts b/src/blocks/create_directory.ts index 87888ed..7e9f25f 100644 --- a/src/blocks/create_directory.ts +++ b/src/blocks/create_directory.ts @@ -1,9 +1,14 @@ import { builtin } from '../ansible/tasks/index.js' -export function create_directory(path: string) { - return builtin.file( - `Create directory ${path}`, - { path, state: 'directory', owner: '{{ansible_user}}', group: '{{ansible_user}}', mode: '0755' }, - { become: true }, - ) +type Config = { + owner?: string + group?: string + mode?: string +} + +export function create_directory(path: string, config: Config = {}) { + config.owner ||= '{{ansible_user}}' + config.group ||= '{{ansible_user}}' + config.mode ||= '0755' + return builtin.file(`Create directory ${path}`, { path, state: 'directory', ...config }, { become: true }) } diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 9e3953b..b2a0ef1 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -1,7 +1,7 @@ export * as assert from './assert.js' export * from './build_repo.js' export * from './create_directory.js' -export * from './create_domain.js' +export * from './create_caddy_domain.js' export * from './create_service.js' export * from './delete_directory.js' export * from './delete_docker_image.js' diff --git a/src/blocks/install_caddy.ts b/src/blocks/install_caddy.ts index bd8796f..499cfeb 100644 --- a/src/blocks/install_caddy.ts +++ b/src/blocks/install_caddy.ts @@ -1,8 +1,12 @@ +import { block } from './block.js' import { Block } from '../ansible/types.js' import { builtin } from '../ansible/tasks/index.js' -import { block } from './block.js' -export function install_caddy(caddyfiles_pattern: string): Block { +type Config = { + caddyfile_content: string +} + +export function install_caddy(config: Config): Block { return block(`Install Caddy`, {}, [ builtin.apt( `Install Caddy's dependencies`, @@ -26,14 +30,7 @@ export function install_caddy(caddyfiles_pattern: string): Block { ), builtin.apt(`Update apt cache`, { update_cache: true }, { become: true }), builtin.apt(`Install Caddy`, { name: 'caddy', state: 'present' }, { become: true }), - builtin.copy( - `Configure Caddy`, - { - dest: '/etc/caddy/Caddyfile', - content: `import ${caddyfiles_pattern}\n`, - }, - { become: true }, - ), + builtin.copy(`Configure Caddy`, { dest: '/etc/caddy/Caddyfile', content: config.caddyfile_content }, { become: true }), builtin.command(`Reload Caddy config`, { cmd: `sudo systemctl start caddy` }, { become: true }), ]).get() } diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..3d14a33 --- /dev/null +++ b/src/files.ts @@ -0,0 +1,25 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +type FileData = { + server_caddyfile: { + log_path: string + service_caddyfiles_pattern: string + } + service_caddyfile: { + domain: string + local_urls: string + } +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const files_dir = path.join(__dirname, 'files') + +export function get_file(name: Name, data: FileData[Name]) { + let content = fs.readFileSync(path.join(files_dir, name), 'utf-8') + for (const [key, value] of Object.entries(data)) { + content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value) + } + return content +} diff --git a/src/files/server_caddyfile b/src/files/server_caddyfile new file mode 100644 index 0000000..14e6097 --- /dev/null +++ b/src/files/server_caddyfile @@ -0,0 +1,11 @@ +{ + log { + format json + output file {{log_path}} { + roll_size 10mb + roll_keep 10 + } + } +} + +import {{service_caddyfiles_pattern}} diff --git a/src/files/service_caddyfile b/src/files/service_caddyfile new file mode 100644 index 0000000..659d7a9 --- /dev/null +++ b/src/files/service_caddyfile @@ -0,0 +1,5 @@ +{{domain}} { + reverse_proxy {{local_urls}} { + lb_policy client_ip_hash + } +} \ No newline at end of file diff --git a/src/reverse_proxy/caddy.ts b/src/reverse_proxy/caddy.ts new file mode 100644 index 0000000..9d4ee85 --- /dev/null +++ b/src/reverse_proxy/caddy.ts @@ -0,0 +1,36 @@ +import path from 'path' +import { get_file } from '../files.js' +import * as blocks from '../blocks/index.js' +import { CaddyConfig, ReverseProxy, ReverseProxyConfig, Server } from '../types.js' + +const default_config: Required = { + get_server_caddyfile: (server) => + get_file('server_caddyfile', { + log_path: `${server.logs_dir}/caddy.log`, + service_caddyfiles_pattern: `${server.services_dir}/*/Caddyfile`, + }), + get_service_caddyfile: (server, config) => get_file('service_caddyfile', config), +} + +export function caddy(config: CaddyConfig = {}): ReverseProxy { + const normalized_config = { ...default_config, ...config } + return { + get_log_path: (server) => `${server.logs_dir}/caddy.log`, + get_server_tasks: (server) => get_server_tasks(normalized_config, server), + get_service_tasks: (server, reverse_proxy_config) => get_service_tasks(normalized_config, server, reverse_proxy_config), + } +} + +function get_server_tasks(config: Required, server: Server) { + return [blocks.install_caddy({ caddyfile_content: config.get_server_caddyfile(server) })] +} + +function get_service_tasks(config: Required, server: Server, reverse_proxy_config: ReverseProxyConfig) { + return [ + blocks.create_caddy_domain({ + domain: reverse_proxy_config.domain, + caddyfile_path: path.join(server.get_service_dir(reverse_proxy_config.service_name), 'Caddyfile'), + caddyfile_content: config.get_service_caddyfile(server, reverse_proxy_config), + }), + ] +} diff --git a/src/reverse_proxy/index.ts b/src/reverse_proxy/index.ts new file mode 100644 index 0000000..6fc277e --- /dev/null +++ b/src/reverse_proxy/index.ts @@ -0,0 +1 @@ +export * from './caddy.js' diff --git a/src/server.ts b/src/server.ts index 7b3cca3..c009c49 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,7 @@ import path from 'path' import { Host } from './ansible/types.js' import * as blocks from './blocks/index.js' import { Server, ServerConfig } from './types.js' +import { caddy } from './reverse_proxy/index.js' export function server(config: ServerConfig): Server { const user = os.userInfo().username @@ -14,16 +15,20 @@ export function server(config: ServerConfig): Server { const hosty_dir = config.hosty_dir || '/srv/hosty' const backups_dir = path.join(hosty_dir, 'backups') const services_dir = path.join(hosty_dir, 'services') + const logs_dir = path.join(hosty_dir, 'logs') + return { connection, hosty_dir, backups_dir, services_dir, + logs_dir, name: config.name, ssh_key: config.ssh_key || { path: '~/.ssh/id_rsa', passphrase: '' }, git_config: config.git_config || {}, docker_network: config.docker_network || 'hosty', docker_prefix: config.docker_prefix || '', + reverse_proxy: config.reverse_proxy || caddy(), get_service_dir: (name) => path.join(services_dir, name), get_backups_dir: (name) => path.join(backups_dir, name), } @@ -58,6 +63,7 @@ export function get_setup_tasks(server: Server) { blocks.generate_ssh_key(server.ssh_key), blocks.install_nixpacks(), blocks.create_directory(server.hosty_dir), - blocks.install_caddy(`${server.services_dir}/*/Caddyfile`), + blocks.create_directory(server.logs_dir, { mode: '0777' }), + ...server.reverse_proxy.get_server_tasks(server), ] } diff --git a/src/services/app/git.ts b/src/services/app/git.ts index c594e1f..ba72f4a 100644 --- a/src/services/app/git.ts +++ b/src/services/app/git.ts @@ -45,7 +45,13 @@ function get_deploy_tasks(server: Server, config: GitAppConfig): Tasks { tasks.push(service) if (config.domain) { - tasks.push(blocks.create_domain({ domain: config.domain, ports_var: 'app_ports', caddyfile_path: path.join(service_dir, 'Caddyfile') })) + tasks.push( + ...server.reverse_proxy.get_service_tasks(server, { + service_name: config.name, + domain: config.domain, + local_urls: `{{ app_ports | map('regex_replace', '^', '127.0.0.1:') | join(' ') }}`, + }), + ) } return tasks } @@ -68,10 +74,10 @@ function make_composes(config: GitAppConfig) { compose.ports ||= [] const composes = [] - for (let i = 1; i <= config.instances!; i++) { + for (let i = 0; i < config.instances!; i++) { composes.push({ ...compose, - ports: [...compose.ports, `{{app_ports[${i - 1}]}}:80`], + ports: [...compose.ports, `{{app_ports[${i}]}}:80`], }) } return composes diff --git a/src/types.ts b/src/types.ts index cde7b4f..d9a6f31 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,11 +35,30 @@ export type ServerConfig = { docker_network?: string docker_prefix?: string connection?: LocalConnection | SshConnection | DockerConnection + reverse_proxy?: ReverseProxy +} + +export type ReverseProxy = { + get_log_path: (server: Server) => string + get_server_tasks: (server: Server) => Tasks + get_service_tasks: (server: Server, config: ReverseProxyConfig) => Tasks +} + +export type ReverseProxyConfig = { + service_name: string + domain: string + local_urls: string +} + +export type CaddyConfig = { + get_server_caddyfile?: (server: Server) => string + get_service_caddyfile?: (server: Server, config: ReverseProxyConfig) => string } export type Server = Required & { services_dir: string backups_dir: string + logs_dir: string get_service_dir: (name: string) => string get_backups_dir: (name: string) => string } diff --git a/tasks.todo b/tasks.todo index 4504af2..a5f40a9 100644 --- a/tasks.todo +++ b/tasks.todo @@ -1,30 +1,20 @@ +proxies: + ✔ make the proxy configurable @done + ✔ add caddy proxy (default) @done + ☐ add nginx proxy + +monitoring: + ☐ make monitoring configurable + ☐ add vector (default) + ☐ add fluentbit? + databases: - ✔ postgres @done - ✔ mysql @done - ✔ redis @done ☐ mongodb features: ☐ auto backups -app.git: - ✔ clone, package, run - ✔ redo only on change - ✔ specific branch - ✔ custom dockerfile @done - ✔ synchronous commands/asserts during deploy @done - ✔ number of instances @done - -add destroy: - ✔ add a `destroy` method to undo deploy of a service, app, ... @done - ✔ update tests to support destroy @done - -commands: - ✔ run commands before starting/restarting an app @done - ✔ run commands after starting/restarting an app @done - ✔ run commands periodically (a cron job) @done - ✔ choose to run the command inside a container or in the host machine @done - -☐ try monitoring with vector and axiom +apps: + ☐ add `app.dir` to deploy from a local or ssh directory github actions: ☐ Create an example repo: @@ -33,10 +23,5 @@ github actions: how to duplicate related containers like db? test apps: - ✔ static @done - ✔ Laravel @done - ✔ Adonis @done - ✔ Nextjs @done - ✔ Rust @done ☐ Remix ☐ Wordpress diff --git a/tests/app-laravel-mysql-custom-docker.test.ts b/tests/app-laravel-mysql-custom-docker.test.ts index 3828c04..f2365a3 100644 --- a/tests/app-laravel-mysql-custom-docker.test.ts +++ b/tests/app-laravel-mysql-custom-docker.test.ts @@ -77,5 +77,6 @@ test('app: laravel + mysql + custom dockerfile', async ({ deploy, destroy, asser assert.file(`/srv/hosty/services/laravel-app`, { exists: false }) assert.command(`docker ps -q --filter "name=laravel-app-1"`, { stdout: '' }, { become: true }) assert.command(`docker ps -q --filter "name=laravel-db"`, { stdout: '' }, { become: true }) + assert.command(`sleep 10`, { stdout: '' }) assert.command(`curl -k https://laravel.local`, { success: false, stderr_contains: 'Could not resolve host: laravel.local' }) }) From 5cb904ed33acc2f932cd0014db1eb0d9189d45d4 Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Sun, 20 Apr 2025 01:23:41 +0200 Subject: [PATCH 2/4] Use a docker container for Caddy --- README.md | 2 +- about.md | 143 ++++++++++++++++++ docs/upgrade.md | 83 ++++++++++ src/blocks/create_caddy_domain.ts | 1 - src/blocks/index.ts | 2 - src/blocks/install_caddy.ts | 36 ----- src/blocks/install_git.ts | 2 +- src/blocks/set_available_port.ts | 59 -------- src/files/service_caddyfile | 2 + src/reverse_proxy/caddy.ts | 46 +++++- src/server.ts | 3 - src/services/app/git.ts | 15 +- src/services/container.ts | 4 +- src/types.ts | 3 +- task.md | 86 +++++++++++ tasks.todo | 9 +- tests/app-adonis-sqlite.test.ts | 1 + tests/app-laravel-mysql-custom-docker.test.ts | 2 +- tests/app-node-postgres.test.ts | 1 + tests/app-rust-nextjs.test.ts | 1 + tests/setup.test.ts | 1 - 21 files changed, 372 insertions(+), 130 deletions(-) create mode 100644 about.md create mode 100644 docs/upgrade.md delete mode 100644 src/blocks/install_caddy.ts delete mode 100644 src/blocks/set_available_port.ts create mode 100644 task.md diff --git a/README.md b/README.md index ef095c3..aea783d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This code will do the following: # Requirements **On local machine:** - [Ansible](https://www.ansible.com/) (tested with v2.16.6) -- Nodejs (tested with v22.8) +- Node.js (tested with v22.8) **On the server** - A Linux server that uses `apt` and `systemctl` (tested on Ubuntu 22.04) diff --git a/about.md b/about.md new file mode 100644 index 0000000..3be0cd1 --- /dev/null +++ b/about.md @@ -0,0 +1,143 @@ +# About Hosty + +## Overview + +Hosty is a code-based, opinionated way to self-host and manage web applications by generating Ansible playbooks and executing them via SSH. It exposes a fluent TypeScript API to define servers, databases, reverse proxies, and applications, then handles provisioning, configuration, and deployment. + +## Prerequisites + +**Local machine** +- Node.js v22.x or later +- pnpm (preferred) or npm/yarn +- Ansible v2.16.x +- zx (included as a devDependency; installed via `pnpm install`) +- TypeScript v5.x + +**Target server** +- Linux distro with `apt` and `systemctl` (e.g., Ubuntu 22.04) +- Non-root user with `sudo` privileges +- SSH access + +## Installation & Build + +```sh +git clone https://github.com/webNeat/hosty.git +cd hosty +pnpm install +pnpm run build +pnpm test +``` + +## Project Structure + +```text +hosty/ +├── about.md # This file: detailed developer guide +├── README.md # Quick start and overview +├── plan.md # Roadmap and design notes +├── examples/ # Usage examples (TypeScript snippets) +├── src/ # Source code (TypeScript modules) +│ ├── ansible/ # Playbook and task generators +│ ├── blocks/ # Reusable Ansible block definitions +│ ├── reverse_proxy/ # Reverse proxy implementations (Caddy, etc.) +│ ├── services/ # App and database resource definitions +│ ├── ci.ts # GitHub Actions handler script +│ ├── compose.types.ts # Types for Docker/compose definitions +│ ├── files.ts # File system utilities +│ ├── index.ts # Public API exports +│ ├── instance.ts # Server instance and playbook logic +│ ├── server.ts # VPS/host definition +│ ├── types.ts # Shared TypeScript types/interfaces +│ └── utils.ts # Helper functions +├── tests/ # Unit and integration tests (*.test.ts) +├── action.yml # GitHub Action composite definition +├── package.json # NPM scripts, dependencies, metadata +├── tsconfig.json # TypeScript compiler settings +├── tsconfig.build.json # Build-specific TS config +├── .prettierrc # Prettier formatting rules +├── .github/ # Workflows and issue templates +└── ... # Other config and lock files +``` + +### Where to Add Code + +- **Core features**: Add new modules under `src/`. Organize by domain: + - Ansible logic: `src/ansible/` or `src/blocks/` + - Service providers (apps, DBs): `src/services/` + - Reverse proxies: `src/reverse_proxy/` + - Export APIs in `src/index.ts` +- **Tests**: Place in `tests/` named `feature.test.ts` +- **Examples**: Add in `examples/` as isolated scripts +- **CI/GitHub Actions**: Update `src/ci.ts` and `action.yml` for automation + +## Coding Conventions & Style + +- **Language**: TypeScript (ESM, via `"type": "module"`) +- **Naming**: Files and directories are lowercase (snake_case for multi-word names). Compose-related types live in `*.types.ts`; shared types live in `types.ts` per directory. Variables and functions use snake_case. Use PascalCase for interfaces and type aliases; no classes are used. +- **Formatting**: Prettier (`.prettierrc`): + - `semi: false` + - `singleQuote: true` + - `printWidth: 155` + - `trailingComma: all` +- **Imports/Exports**: Use named ESM imports and exports; avoid default exports in public API +- **Public API**: All top-level functions/types must be exported in `src/index.ts` +- **Indentation**: 2 spaces per level (no tabs) +- **Import File Extensions**: Include `.js` extension in all import paths to satisfy Node ESM (e.g. `import {foo} from './bar.js'`) +- **Async/Await**: Use async/await for all promise-based code; avoid callbacks +- **ES2020 Features**: Prefer nullish coalescing (`??`), optional chaining, and logical assignments (`||=`, `&&=`) for defaults and assignments +- **Error Handling**: Throw clear errors with messages prefixed by `webNeat/hosty:` in scripts and CI +- **Named Exports Only**: Avoid default exports; use named exports consistently +- **Ansible DSL**: Define tasks via `src/blocks/*` or `ansible.tasks.builtin`, never inline raw YAML +- **TypeScript Strictness**: Adhere to `strict: true`, `forceConsistentCasingInFileNames`, and other compiler options in `tsconfig.json` + +## API Reference + +- `server(config: ServerConfig)` – Define a VPS or host +- `database.postgres(config: PostgresConfig)` – Create a Postgres resource +- `app.git(config: GitAppConfig)` – Deploy an app from Git +- `reverse_proxy.caddy(config: CaddyConfig)` – Configure Caddy +- `deploy(...resources)` – Generate `hosty-playbook.yaml` +- `run()` – Execute the generated Ansible playbook + +See `src/types.ts` and individual modules for full signatures. + +## Development Workflow + +1. Implement or modify code in `src/` +2. Add or update tests in `tests/` +3. Run `pnpm run build` to compile +4. Run `pnpm test` to verify +5. Update docs (`README.md`, `about.md`, `plan.md`) if needed +6. Commit and open a pull request + +## Testing + +- Tests are written in TypeScript using built-in assertions or frameworks +- Execute via `pnpm test` (runs `node --import tsx test.ts`) + +## Examples + +Browse `examples/` for sample scripts: +- `app-node-postgres/` +- `app-laravel-mysql/` +- `app-rust-nextjs/` + +Copy and customize these to your setup. + +## GitHub Action + +Defined in `action.yml` and implemented in `src/ci.ts`: +- **Inputs**: `server_ip`, `server_user`, `ssh_private_key`, `server_sudo_pass`, `handler`, `vars`, `verbose` +- **Steps**: Set up SSH, generate playbook, run `ansible-playbook` with `hosty-playbook.yaml` + +## Contributing + +- Fork and PR against `main` +- Adhere to code style and add tests +- Ensure CI passes before merge + +## Resources + +- **Roadmap**: `plan.md` +- **Quick start**: `README.md` +- **File list**: `files.txt` diff --git a/docs/upgrade.md b/docs/upgrade.md new file mode 100644 index 0000000..07b0d21 --- /dev/null +++ b/docs/upgrade.md @@ -0,0 +1,83 @@ +# Upgrade Guide: Migrating to Container-based Caddy + +This guide walks you through migrating from system-level Caddy to the Docker-based Caddy setup. + +## 1. Copy system Caddy config + +Before uninstalling, copy your existing Caddy config into the Hosty directory: + +```bash +sudo mkdir -p /srv/hosty/caddy/config /srv/hosty/caddy/includes /srv/hosty/caddy/logs +sudo cp /etc/caddy/Caddyfile /srv/hosty/caddy/config/ +sudo cp -r /etc/caddy/certs /srv/hosty/caddy/config/ +sudo cp /etc/caddy/includes/*.Caddyfile /srv/hosty/caddy/includes/ +``` + +## 2. Stop and Remove System Caddy + +Run: + +```bash +sudo systemctl stop caddy +sudo apt remove --purge caddy +``` + +Remove residual files: + +```bash +sudo rm -rf /etc/caddy /var/log/caddy +``` + +This stops and uninstalls the legacy system‑level Caddy package. + +## 3. Update DNS or /etc/hosts + +Ensure your domains point to the host machine: + +```bash +# Example for a local test domain +echo "127.0.0.1 example.local" | sudo tee -a /etc/hosts +``` + +## 4. Deploy Container-based Caddy + +Re-run your Hosty script: + +```bash +deploy(vps, /* your services */) +run({ ask_sudo_pass: true }) +``` + +This will start the Caddy Docker container, load your existing config, and serve your sites with Docker-managed TLS. + +## 5. Automate Migration with Hosty + +You can automate the migration via Hosty's `tasks` helper: + +```ts +import path from 'path' +import { server, tasks, deploy, run, blocks, ansible } from 'hosty' +const { builtin } = ansible.tasks + +const vps = server({ name: 'your-server-ip-or-hostname' }) +const caddy_dir = path.join(vps.hosty_dir, 'caddy') +deploy( + vps, + tasks( + blocks.create_directory(caddy_dir), + blocks.create_directory(path.join(caddy_dir, 'config')), + blocks.create_directory(path.join(caddy_dir, 'includes')), + blocks.create_directory(path.join(caddy_dir, 'logs')), + builtin.copy('Copy Caddyfile', { src: '/etc/caddy/Caddyfile', dest: path.join(caddy_dir, 'config/Caddyfile') }), + builtin.copy('Copy certificates', { src: '/etc/caddy/certs', dest: path.join(caddy_dir, 'config/certs'), recursive: true }), + builtin.copy('Copy includes', { src: '/etc/caddy/includes', dest: path.join(caddy_dir, 'includes'), recursive: true }), + builtin.command('Stop and remove system Caddy', { cmd: 'sudo systemctl stop caddy && sudo apt remove --purge caddy' }, { become: true }), + blocks.delete_directory(path.join(caddy_dir, 'config')), + blocks.delete_directory(path.join(caddy_dir, 'includes')), + blocks.delete_directory(path.join(caddy_dir, 'logs')), + ) +) +run({ ask_sudo_pass: true }) +``` + +That's it! Your migration is fully automated and your setup is containerized under Hosty. diff --git a/src/blocks/create_caddy_domain.ts b/src/blocks/create_caddy_domain.ts index ac26ff1..c578bcc 100644 --- a/src/blocks/create_caddy_domain.ts +++ b/src/blocks/create_caddy_domain.ts @@ -16,6 +16,5 @@ export function create_caddy_domain(config: Config): Block { { become: true }, ), builtin.copy(`Create Caddyfile for ${config.domain}`, { dest: config.caddyfile_path, content: config.caddyfile_content }, { register: 'caddyfile' }), - builtin.command(`Reload caddy`, { cmd: `sudo systemctl reload caddy` }, { become: true, when: 'caddyfile.changed' }), ]).get() } diff --git a/src/blocks/index.ts b/src/blocks/index.ts index b2a0ef1..5a752cc 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -8,8 +8,6 @@ export * from './delete_docker_image.js' export * from './delete_domain.js' export * from './delete_service.js' export * from './generate_ssh_key.js' -export * from './install_caddy.js' export * from './install_docker.js' export * from './install_git.js' export * from './install_nixpacks.js' -export * from './set_available_port.js' diff --git a/src/blocks/install_caddy.ts b/src/blocks/install_caddy.ts deleted file mode 100644 index 499cfeb..0000000 --- a/src/blocks/install_caddy.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { block } from './block.js' -import { Block } from '../ansible/types.js' -import { builtin } from '../ansible/tasks/index.js' - -type Config = { - caddyfile_content: string -} - -export function install_caddy(config: Config): Block { - return block(`Install Caddy`, {}, [ - builtin.apt( - `Install Caddy's dependencies`, - { name: ['debian-keyring', 'debian-archive-keyring', 'apt-transport-https', 'curl'], state: 'present' }, - { become: true }, - ), - builtin.shell( - `Add Caddy's official GPG key`, - { - cmd: `curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg`, - }, - { become: true }, - ), - builtin.shell( - `Add Caddy's apt repository`, - { - cmd: `curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list`, - creates: `/etc/apt/sources.list.d/caddy-stable.list`, - }, - { become: true }, - ), - builtin.apt(`Update apt cache`, { update_cache: true }, { become: true }), - builtin.apt(`Install Caddy`, { name: 'caddy', state: 'present' }, { become: true }), - builtin.copy(`Configure Caddy`, { dest: '/etc/caddy/Caddyfile', content: config.caddyfile_content }, { become: true }), - builtin.command(`Reload Caddy config`, { cmd: `sudo systemctl start caddy` }, { become: true }), - ]).get() -} diff --git a/src/blocks/install_git.ts b/src/blocks/install_git.ts index ec24f55..aa9a1f8 100644 --- a/src/blocks/install_git.ts +++ b/src/blocks/install_git.ts @@ -11,6 +11,6 @@ export function install_git({ name, email }: Config): Block { const x = block(`Install and configure Git`) x.add(builtin.apt('Install git', { name: 'git', state: 'present', update_cache: true, cache_valid_time: 3600 }, { become: true })) if (name) x.add(general.git_config(`Set git user.name to ${name} globally`, { name: 'user.name', value: name, scope: 'global' })) - if (email) x.add(general.git_config(`Set git user.email to ${name} globally`, { name: 'user.email', value: email, scope: 'global' })) + if (email) x.add(general.git_config(`Set git user.email to ${email} globally`, { name: 'user.email', value: email, scope: 'global' })) return x.get() } diff --git a/src/blocks/set_available_port.ts b/src/blocks/set_available_port.ts deleted file mode 100644 index 3ca83ba..0000000 --- a/src/blocks/set_available_port.ts +++ /dev/null @@ -1,59 +0,0 @@ -import path from 'path' -import { block } from './block.js' -import { builtin } from '../ansible/tasks/index.js' -import { create_directory } from './create_directory.js' - -export function set_available_ports(service_dir: string, count: number, var_name: string) { - const port_file = path.join(service_dir, '.ports') - const cmd = ` - # Initialize variables - ports=() - existing_ports=() - count=0 - desired_count=${count} - - # Read existing ports from the file, if it exists - if [ -f "${port_file}" ]; then - while IFS= read -r line; do - existing_ports+=("$line") - done < "${port_file}" - fi - - # Add existing ports to the final list - for port in "\${existing_ports[@]}"; do - ports+=("$port") - count=$((count + 1)) - [ $count -eq $desired_count ] && break - done - - # If we still need more ports, find and add available ones - if [ $count -lt $desired_count ]; then - for port in $(seq 8000 9000); do - # Skip ports already in the list - if [[ " \${ports[*]} " == *" $port "* ]]; then - continue - fi - (echo >/dev/tcp/localhost/$port) &>/dev/null && continue || ports+=($port) - count=$((count + 1)) - [ $count -eq $desired_count ] && break - done - fi - - # If we have too many ports, trim the list - if [ $count -gt $desired_count ]; then - ports=("\${ports[@]:0:$desired_count}") - fi - - # Write the ports to the file, one per line - > "${port_file}" # Clear the file before writing - for port in "\${ports[@]}"; do - echo "$port" >> "${port_file}" - done - ` - return block(`Generate ${count} available ports for ${service_dir} into the var ${var_name}`, {}, [ - create_directory(service_dir), - builtin.shell(`Generate an available port in ${port_file}`, { cmd, executable: '/bin/bash' }), - builtin.command(`Read the ports from ${port_file}`, { cmd: `cat ${port_file}` }, { register: 'cat_ports' }), - builtin.set_facts(`Set the ports in the var ${var_name}`, { [var_name]: `{{cat_ports.stdout_lines}}` }), - ]).get() -} diff --git a/src/files/service_caddyfile b/src/files/service_caddyfile index 659d7a9..3ab4d4e 100644 --- a/src/files/service_caddyfile +++ b/src/files/service_caddyfile @@ -1,4 +1,6 @@ {{domain}} { + # Use self-signed cert for local .local domains + tls internal reverse_proxy {{local_urls}} { lb_policy client_ip_hash } diff --git a/src/reverse_proxy/caddy.ts b/src/reverse_proxy/caddy.ts index 9d4ee85..9321bc1 100644 --- a/src/reverse_proxy/caddy.ts +++ b/src/reverse_proxy/caddy.ts @@ -1,36 +1,70 @@ import path from 'path' import { get_file } from '../files.js' import * as blocks from '../blocks/index.js' +import { builtin } from '../ansible/tasks/index.js' import { CaddyConfig, ReverseProxy, ReverseProxyConfig, Server } from '../types.js' const default_config: Required = { get_server_caddyfile: (server) => get_file('server_caddyfile', { - log_path: `${server.logs_dir}/caddy.log`, - service_caddyfiles_pattern: `${server.services_dir}/*/Caddyfile`, + log_path: path.join(server.hosty_dir, 'caddy', 'logs', 'caddy.log'), + service_caddyfiles_pattern: path.join(server.hosty_dir, 'caddy', 'includes', '*.Caddyfile'), }), - get_service_caddyfile: (server, config) => get_file('service_caddyfile', config), + get_service_caddyfile: (server, config) => { + const local_urls = Array.from({ length: config.instances }, (_, i) => `${config.service_name}-${i + 1}:80`).join(' ') + return get_file('service_caddyfile', { domain: config.domain, local_urls }) + }, } export function caddy(config: CaddyConfig = {}): ReverseProxy { const normalized_config = { ...default_config, ...config } return { - get_log_path: (server) => `${server.logs_dir}/caddy.log`, + get_log_path: (server) => path.join(server.hosty_dir, 'caddy', 'logs', 'caddy.log'), get_server_tasks: (server) => get_server_tasks(normalized_config, server), get_service_tasks: (server, reverse_proxy_config) => get_service_tasks(normalized_config, server, reverse_proxy_config), } } function get_server_tasks(config: Required, server: Server) { - return [blocks.install_caddy({ caddyfile_content: config.get_server_caddyfile(server) })] + return [ + blocks.create_directory(path.join(server.hosty_dir, 'caddy/config')), + blocks.create_directory(path.join(server.hosty_dir, 'caddy/includes')), + blocks.create_directory(path.join(server.hosty_dir, 'caddy/logs')), + builtin.copy('Create dummy Caddy include', { dest: path.join(server.hosty_dir, 'caddy/includes/.dummy.Caddyfile'), content: '' }, { become: true }), + builtin.copy( + 'Create master Caddyfile', + { content: 'import includes/*.Caddyfile', dest: path.join(server.hosty_dir, 'caddy/config/Caddyfile') }, + { register: 'caddy_master' }, + ), + blocks.create_service({ + name: 'caddy', + service_dir: `${server.hosty_dir}/caddy`, + docker_network: server.docker_network, + compose: { + image: 'caddy:2.9-alpine', + ports: ['80:80', '443:443'], + networks: [server.docker_network], + volumes: [ + `${server.hosty_dir}/caddy/config:/etc/caddy`, + `${server.hosty_dir}/caddy/includes:/etc/caddy/includes:ro`, + `${server.hosty_dir}/caddy/logs:/var/log/caddy`, + ], + }, + }), + ] } function get_service_tasks(config: Required, server: Server, reverse_proxy_config: ReverseProxyConfig) { return [ blocks.create_caddy_domain({ domain: reverse_proxy_config.domain, - caddyfile_path: path.join(server.get_service_dir(reverse_proxy_config.service_name), 'Caddyfile'), + caddyfile_path: path.join(server.hosty_dir, `caddy/includes/${reverse_proxy_config.service_name}.Caddyfile`), caddyfile_content: config.get_service_caddyfile(server, reverse_proxy_config), }), + builtin.command( + `Reload Caddy container`, + { cmd: `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` }, + { become: true, when: 'caddyfile.changed' }, + ), ] } diff --git a/src/server.ts b/src/server.ts index c009c49..a3d35ca 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,14 +15,12 @@ export function server(config: ServerConfig): Server { const hosty_dir = config.hosty_dir || '/srv/hosty' const backups_dir = path.join(hosty_dir, 'backups') const services_dir = path.join(hosty_dir, 'services') - const logs_dir = path.join(hosty_dir, 'logs') return { connection, hosty_dir, backups_dir, services_dir, - logs_dir, name: config.name, ssh_key: config.ssh_key || { path: '~/.ssh/id_rsa', passphrase: '' }, git_config: config.git_config || {}, @@ -63,7 +61,6 @@ export function get_setup_tasks(server: Server) { blocks.generate_ssh_key(server.ssh_key), blocks.install_nixpacks(), blocks.create_directory(server.hosty_dir), - blocks.create_directory(server.logs_dir, { mode: '0777' }), ...server.reverse_proxy.get_server_tasks(server), ] } diff --git a/src/services/app/git.ts b/src/services/app/git.ts index ba72f4a..fa5fc35 100644 --- a/src/services/app/git.ts +++ b/src/services/app/git.ts @@ -17,11 +17,6 @@ function get_deploy_tasks(server: Server, config: GitAppConfig): Tasks { config.instances ||= 1 const tasks: Tasks = [] const service_dir = path.join(server.hosty_dir, 'services', config.name) - - if (config.domain) { - tasks.push(blocks.set_available_ports(service_dir, config.instances, 'app_ports')) - } - tasks.push( blocks.build_repo({ repo_url: config.repo, @@ -49,7 +44,7 @@ function get_deploy_tasks(server: Server, config: GitAppConfig): Tasks { ...server.reverse_proxy.get_service_tasks(server, { service_name: config.name, domain: config.domain, - local_urls: `{{ app_ports | map('regex_replace', '^', '127.0.0.1:') | join(' ') }}`, + instances: config.instances, }), ) } @@ -71,14 +66,10 @@ function make_composes(config: GitAppConfig) { const compose = config.compose || {} compose.image = config.name compose.environment = { ...(config.env || {}), ...(compose.environment || {}) } - compose.ports ||= [] - const composes = [] + const composes: (typeof compose)[] = [] for (let i = 0; i < config.instances!; i++) { - composes.push({ - ...compose, - ports: [...compose.ports, `{{app_ports[${i}]}}:80`], - }) + composes.push({ ...compose }) } return composes } diff --git a/src/services/container.ts b/src/services/container.ts index a0b6837..3e19552 100644 --- a/src/services/container.ts +++ b/src/services/container.ts @@ -20,12 +20,12 @@ function get_deploy_tasks(server: Server, { name, compose, files_dir, files }: C files_dir, files, docker_network: server.docker_network, - service_dir: path.join(server.hosty_dir, '/services', name), + service_dir: path.join(server.hosty_dir, 'services', name), restart_conditions: [], }), ] } function get_destroy_tasks(server: Server, { name }: ContainerConfig): Tasks { - return [blocks.delete_service(path.join(server.hosty_dir, '/services', name))] + return [blocks.delete_service(path.join(server.hosty_dir, 'services', name))] } diff --git a/src/types.ts b/src/types.ts index d9a6f31..61969c9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -47,7 +47,7 @@ export type ReverseProxy = { export type ReverseProxyConfig = { service_name: string domain: string - local_urls: string + instances: number } export type CaddyConfig = { @@ -58,7 +58,6 @@ export type CaddyConfig = { export type Server = Required & { services_dir: string backups_dir: string - logs_dir: string get_service_dir: (name: string) => string get_backups_dir: (name: string) => string } diff --git a/task.md b/task.md new file mode 100644 index 0000000..3837ac4 --- /dev/null +++ b/task.md @@ -0,0 +1,86 @@ +# Implementing Caddy as a Service + +## Overview + +Right now, Caddy is installed on the server. I would like to use it in a docker container instead. + +## Example Usage + +```ts +import {server, reverse_proxy, app, database, deploy} from 'hosty' + +const vps = server({ + name: 'vps', + reverse_proxy: reverse_proxy.caddy({ + version: '2.9-alpine', // Optional, default is 2.9-alpine + config: ``, // Optional, default to some global config + }), +}) + +const db = database.postgres({ name: 'db' }) +const demo = app.git({ + name: 'demo', + domain: 'demo.example.com', +}) + +deploy(vps, db, demo) +``` + +## Detailed Implementation Plan + +1. Compose-based Caddy Service Block + - Define `CaddyServiceConfig` type to capture `version`, `image`, `ports`, `networks`, `volumes`, and `before_start` commands. + - In `src/reverse_proxy/caddy.ts#get_server_tasks`, replace the `blocks.install_caddy` block with: + ```ts + blocks.create_service({ + name: 'caddy', + service_dir: `${server.hosty_dir}/caddy`, + compose: { + image: `caddy:${config.version || '2.9-alpine'}`, + ports: ['80:80', '443:443'], + networks: [server.docker_network], + volumes: [ + `${server.hosty_dir}/caddy/config:/etc/caddy`, + `${server.hosty_dir}/caddy/includes:/etc/caddy/includes:ro`, + `${server.hosty_dir}/caddy/logs:/var/log/caddy`, + ], + }, + }) + ``` +2. Mounting and Directories + - Ensure host dirs exist via `blocks.create_directory` in `get_setup_tasks` for: + - `${server.hosty_dir}/caddy/config` + - `${server.hosty_dir}/caddy/includes` + - `${server.hosty_dir}/caddy/logs` +3. Networking + - Use `server.docker_network` for inter-service routing. + - Expose ports 80/443 in compose service. +4. Caddyfile Provisioning + - Generate a master Caddyfile at `${server.hosty_dir}/caddy/config/Caddyfile` containing: + ```caddyfile + import includes/*.Caddyfile + ``` + - In `src/reverse_proxy/caddy.ts#get_service_tasks`, after creating each service's Caddyfile, add a symlink task using `reverse_proxy_config.service_name`: + ```ts + blocks.command( + `Link ${reverse_proxy_config.service_name} Caddyfile`, + { cmd: `ln -sf ${server.get_service_dir(reverse_proxy_config.service_name)}/Caddyfile ${server.hosty_dir}/caddy/includes/${reverse_proxy_config.service_name}.Caddyfile` }, + { become: true } + ), + ``` +5. Refactor Code + - Delete `src/blocks/install_caddy.ts` and remove its imports. + - Remove creation and references to the global `logs_dir` (no longer needed) + - Update docs (`README.md`, `about.md`) to reflect container-based Caddy. +6. Migration and Backward Compatibility + - Detect legacy system-level Caddy and warn users if present. + - Provide a migration guide in `docs/upgrade.md` detailing: + - Stopping/destroying old Caddy (`hosty destroy reverse_proxy caddy`). + - Copying/moving existing Caddyfiles and certs into `caddy/config` and `caddy/includes`. + - Removing old logs_dir and deprecated tasks. +7. Testing & CI + - Add `tests/reverse_proxy_caddy_container.test.ts`: + - `deploy` should start Caddy container, verify via `docker ps`. + - Perform HTTP(S) requests to confirm routing. + - `destroy` should stop/remove container and clean files. + - Update GitHub Action to include new tests in CI pipeline. \ No newline at end of file diff --git a/tasks.todo b/tasks.todo index a5f40a9..22acfe2 100644 --- a/tasks.todo +++ b/tasks.todo @@ -1,12 +1,14 @@ proxies: ✔ make the proxy configurable @done - ✔ add caddy proxy (default) @done - ☐ add nginx proxy + ✔ add caddy proxy as container (default) @done + ☐ add nginx proxy as container + +server setup: + ☐ use ufw to only allow ssh, http and https monitoring: ☐ make monitoring configurable ☐ add vector (default) - ☐ add fluentbit? databases: ☐ mongodb @@ -23,5 +25,6 @@ github actions: how to duplicate related containers like db? test apps: + ☐ Nestjs ☐ Remix ☐ Wordpress diff --git a/tests/app-adonis-sqlite.test.ts b/tests/app-adonis-sqlite.test.ts index 5512068..95bfe91 100644 --- a/tests/app-adonis-sqlite.test.ts +++ b/tests/app-adonis-sqlite.test.ts @@ -30,6 +30,7 @@ test('app: adonis + migrations + custom dockerfile', async ({ deploy, destroy, a deploy(api, migration) assert.command(`docker ps --filter "name=adonis-api-1"`, { stdout_contains: 'adonis-api-1' }, { become: true }) + assert.command(`sleep 10`, { success: true }) assert.command(`curl -k https://adonis-api.local`, { success: true, stdout: `{"hello":"world"}` }) assert.command(`curl -k https://adonis-api.local/users`, { success: true, diff --git a/tests/app-laravel-mysql-custom-docker.test.ts b/tests/app-laravel-mysql-custom-docker.test.ts index f2365a3..f3ae210 100644 --- a/tests/app-laravel-mysql-custom-docker.test.ts +++ b/tests/app-laravel-mysql-custom-docker.test.ts @@ -77,6 +77,6 @@ test('app: laravel + mysql + custom dockerfile', async ({ deploy, destroy, asser assert.file(`/srv/hosty/services/laravel-app`, { exists: false }) assert.command(`docker ps -q --filter "name=laravel-app-1"`, { stdout: '' }, { become: true }) assert.command(`docker ps -q --filter "name=laravel-db"`, { stdout: '' }, { become: true }) - assert.command(`sleep 10`, { stdout: '' }) + assert.command(`sleep 20`, { stdout: '' }) assert.command(`curl -k https://laravel.local`, { success: false, stderr_contains: 'Could not resolve host: laravel.local' }) }) diff --git a/tests/app-node-postgres.test.ts b/tests/app-node-postgres.test.ts index 71cc97f..cd6e135 100644 --- a/tests/app-node-postgres.test.ts +++ b/tests/app-node-postgres.test.ts @@ -20,6 +20,7 @@ test('app: express + postgres', async ({ deploy, destroy, assert }) => { deploy(database, todo_app) assert.command(`docker ps --filter "name=todo-1"`, { stdout_contains: 'todo-1' }, { become: true }) + assert.command(`sleep 10`, { success: true }) assert.command(`curl -k https://todo.local`, { success: true, stdout: '[]' }) assert.command(`curl -k -X POST https://todo.local -H "Content-Type: application/json" -d '{"content":"first task"}'`, { success: true, diff --git a/tests/app-rust-nextjs.test.ts b/tests/app-rust-nextjs.test.ts index 3c17459..e1aa9aa 100644 --- a/tests/app-rust-nextjs.test.ts +++ b/tests/app-rust-nextjs.test.ts @@ -25,6 +25,7 @@ test('app: monorepo rust + nextjs', async ({ deploy, destroy, assert }) => { deploy(api, web) assert.command(`docker ps --filter "name=rust-api-1"`, { stdout_contains: 'rust-api-1' }, { become: true }) assert.command(`docker ps --filter "name=next-web-1"`, { stdout_contains: 'next-web-1' }, { become: true }) + assert.command(`sleep 10`, { success: true }) assert.command(`curl -k https://rust-api.local/greet/foo`, { success: true, stdout: '{"hello":"foo"}' }) assert.command(`curl -k https://rust-api.local/fibonacci/10`, { success: true, stdout: '{"value":55}' }) assert.command(`curl -k https://next-web.local`, { success: true, stdout_contains: '

Fibonacci of 1 is 1

' }) diff --git a/tests/setup.test.ts b/tests/setup.test.ts index 79da2c5..c7c43b7 100644 --- a/tests/setup.test.ts +++ b/tests/setup.test.ts @@ -4,5 +4,4 @@ test('setup', async ({ assert }) => { assert.command(`docker --version`, { success: true }) assert.command(`git --version`, { success: true }) assert.command(`nixpacks --version`, { success: true }) - assert.command(`systemctl is-active caddy`, { stdout: 'active' }) }) From 4f73126c5afc06307a1bb8f4ba82b1e3573f0b8e Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Tue, 27 May 2025 14:18:45 +0200 Subject: [PATCH 3/4] Add Docker env to run tests --- Dockerfile | 52 ++++++ about.md | 296 ++++++++++++++++++++++++++++++ compose.yaml | 7 + pnpm-lock.yaml | 92 ---------- pnpm-workspace.yaml | 2 + src/ansible/tasks/builtin.ts | 2 +- src/blocks/create_caddy_domain.ts | 2 +- src/blocks/delete_domain.ts | 6 +- src/blocks/generate_ssh_key.ts | 2 +- src/types.ts | 2 +- tests/utils/index.ts | 2 +- 11 files changed, 367 insertions(+), 98 deletions(-) create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100644 pnpm-workspace.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00e1992 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg lsb-release && \ + mkdir -p -m 0755 /etc/apt/keyrings && \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ + gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ + > /etc/apt/sources.list.d/docker.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-buildx-plugin && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Nixpacks +RUN curl -sSL https://nixpacks.com/install.sh | bash && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Hosty test dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl gnupg && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y --no-install-recommends \ + nodejs \ + python3 \ + python3-pip \ + ansible \ + git \ + sudo \ + openssh-client && \ + npm install -g pnpm && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -e' \ + '# start daemon in background, redirecting logs' \ + 'dockerd --host=unix:///var/run/docker.sock --data-root=/var/lib/docker > /var/log/dockerd.log 2>&1 &' \ + '# wait until it is ready' \ + 'until docker info >/dev/null 2>&1; do' \ + ' echo "Waiting for Docker daemon to start... (logs in /var/log/dockerd.log)"' \ + ' sleep 0.2' \ + 'done' \ + 'exec "$@"' \ + > /usr/local/bin/dind-entrypoint && chmod +x /usr/local/bin/dind-entrypoint + +VOLUME /var/lib/docker +ENTRYPOINT ["/usr/local/bin/dind-entrypoint"] +CMD ["bash"] diff --git a/about.md b/about.md index 3be0cd1..a7c2208 100644 --- a/about.md +++ b/about.md @@ -4,6 +4,60 @@ Hosty is a code-based, opinionated way to self-host and manage web applications by generating Ansible playbooks and executing them via SSH. It exposes a fluent TypeScript API to define servers, databases, reverse proxies, and applications, then handles provisioning, configuration, and deployment. +## Core Concepts & Workflow + +Hosty bridges the gap between defining your infrastructure in code and having it deployed on your servers. It achieves this by translating your TypeScript declarations into executable Ansible playbooks. + +The typical workflow is as follows: + +1. **Define Resources**: In a TypeScript file (e.g., `deploy.ts`), you use Hosty's API (`app`, `db`, `server`, `reverse_proxy`) to declare the desired state of your infrastructure. This includes: + * Server(s) you want to deploy to. + * Databases (e.g., PostgreSQL). + * Applications (e.g., from a Git repository, potentially as Docker containers). + * Reverse proxy configurations (e.g., Caddy for automatic HTTPS). + +2. **Generate Playbook**: You call the `deploy(...resources)` function, passing in all your defined resources. Hosty processes these definitions and generates an Ansible playbook named `hosty-playbook.yaml` in your project's root directory. This playbook contains all the necessary Ansible tasks to provision, configure, and deploy your resources. + +3. **Execute Playbook**: + * **Locally**: You call the `run()` function. This function invokes `ansible-playbook` to execute the generated `hosty-playbook.yaml` against the target server(s) specified in your definitions. It handles SSH connections and sudo privileges as configured. + * **Via GitHub Actions**: The Hosty GitHub Action (`action.yml`) automates this process. It checks out your code, runs your deployment script (specified by the `handler` input), which generates the playbook, and then executes the playbook. Inputs like `server_ip`, `ssh_private_key`, etc., are used to configure the connection. + +4. **Server Provisioning**: Ansible, guided by the playbook, connects to your server(s) via SSH. It then performs tasks such as: + * Installing necessary software (e.g., Docker, Caddy, PostgreSQL client/server). + * Setting up users and permissions. + * Configuring databases. + * Cloning application repositories. + * Building and running applications (often as Docker containers managed by Docker Compose, based on your app definition). + * Configuring the reverse proxy (e.g., Caddy) to route traffic to your applications and enable HTTPS automatically. + +Hosty's opinionated approach means it makes certain choices for you (e.g., using Caddy, promoting Docker for applications) to simplify the deployment process, while still offering configuration options. + +## Key Components Under the Hood + +Hosty's functionality is built upon several key components working together: + +### 1. TypeScript API & Resource Definitions + - **`src/index.ts`**: Exports the public API functions like `server`, `app.git`, `db.postgres`, `reverse_proxy.caddy`, `deploy`, and `run`. + - **`src/server.ts`**: Defines the `ServerConfig` and logic for representing target servers. + - **`src/services/`**: Contains modules for defining various deployable services: + - `db_postgres.ts` (example): Logic for PostgreSQL database resources. + - `app_git.ts` (example): Logic for applications deployed from Git repositories. This often involves generating Docker Compose configurations. + - `compose.types.ts`: TypeScript definitions related to Docker Compose, indicating that applications are often containerized. + - **`src/reverse_proxy/`**: Handles reverse proxy setup. + - `caddy.ts` (example): Implements Caddy configuration, including domain setup and automatic HTTPS. + +### 2. Ansible Playbook Generation + - **`src/instance.ts`**: This is a central piece that takes the user-defined resources, processes them, and orchestrates the generation of the Ansible playbook (`hosty-playbook.yaml`). It translates the high-level TypeScript definitions into specific Ansible tasks and roles. + - **`src/ansible/`**: Contains utilities and helper functions for generating Ansible tasks and playbook structures in YAML format. It might include templates or functions to create common Ansible constructs. + - **`src/blocks/`**: Defines reusable Ansible blocks or task lists for common operations (e.g., installing a package, managing a service, configuring a user). These blocks are then assembled by `src/instance.ts` into the final playbook. The goal is to avoid writing raw YAML directly and instead use these typed, reusable TypeScript functions. + +### 3. Execution Engine + - The `run()` function (typically called after `deploy()`) or the GitHub Action (`action.yml`) uses the system's `ansible-playbook` command to execute the generated `hosty-playbook.yaml`. + - SSH connection details (IP, user, key, sudo password) are passed to Ansible to enable it to connect to and manage the remote server. + - **`zx` library**: Hosty may use `zx` (listed in devDependencies) internally for scripting interactions with the shell, such as invoking Ansible. + +This architecture allows Hosty to provide a developer-friendly TypeScript interface while leveraging the power and idempotency of Ansible for the actual server configuration and deployment tasks. + ## Prerequisites **Local machine** @@ -57,6 +111,7 @@ hosty/ ├── .prettierrc # Prettier formatting rules ├── .github/ # Workflows and issue templates └── ... # Other config and lock files + ``` ### Where to Add Code @@ -101,6 +156,247 @@ hosty/ See `src/types.ts` and individual modules for full signatures. +## Detailed API Reference + +The public API is exported from `src/index.ts`. Key functions include: + +### `server(config: ServerConfig): Server` +Defines a target server for deployment. + +- **`config: ServerConfig`**: + - `name: string`: A unique name for the server (e.g., IP address or hostname). This is used by Ansible as the inventory host. + - `user?: string`: The SSH user to connect with (defaults to a common user if not provided by GitHub Action). + - `pass?: string`: The sudo password for the user on the server. + - `vars?: Record`: Additional Ansible variables specific to this host. +- **Returns**: A `Server` object representing the configured server. + +**Example:** +```typescript +const myVPS = server({ + name: '192.168.1.100', // IP or hostname + user: 'deploy_user', + pass: 'sudo_password_here' +}); +``` + +### `db.postgres(config: PostgresConfig): PostgresDb` +Defines a PostgreSQL database resource to be provisioned. + +- **`config: PostgresConfig`**: + - `name: string`: The name of the database to create. + - `user: string`: The username for the database. + - `pass: string`: The password for the database user. + - `version?: string`: (Optional) Specify PostgreSQL version. + - `extensions?: string[]`: (Optional) List of PostgreSQL extensions to enable. +- **Returns**: A `PostgresDb` object containing details like generated host, user, pass, and name, which can be used as environment variables for applications. + +**Example:** +```typescript +const mainDb = db.postgres({ + name: 'my_app_db', + user: 'app_user', + pass: 's3cureP@ssw0rd' +}); +``` + +### `app.git(config: GitAppConfig): GitApp` +Defines an application to be deployed from a Git repository. Hosty typically packages these as Docker containers using Docker Compose. + +- **`config: GitAppConfig`**: + - `name: string`: A unique name for the application. + - `repo: string`: The URL of the Git repository. + - `branch?: string`: (Optional) The Git branch to deploy (defaults to `main` or `master`). + - `domain?: string | string[]`: (Optional) The domain(s) to configure for this application via the reverse proxy (e.g., Caddy). If provided, HTTPS is usually set up automatically. + - `env?: Record`: Environment variables for the application. Values can be direct strings/numbers or references to secrets. + - `ports?: string[]`: (Optional) Port mappings (e.g., `"8080:80"`). Often handled by Docker Compose. + - `build?: { command?: string; dockerfile?: string; context?: string }`: (Optional) Build instructions. If a `dockerfile` is specified, Hosty will likely build a Docker image. + - `volumes?: string[]`: (Optional) Docker volume mappings. + - `depends_on?: (GitApp | PostgresDb)[]`: (Optional) Declare dependencies on other resources. +- **Returns**: A `GitApp` object. + +**Example:** +```typescript +const myApi = app.git({ + name: 'my-cool-api', + repo: 'https://github.com/user/my-api.git', + branch: 'develop', + domain: 'api.example.com', + env: { + PORT: '3000', + DATABASE_URL: `postgresql://${mainDb.user}:${mainDb.pass}@${mainDb.host}/${mainDb.name}`, + API_KEY: { secret: 'MY_API_KEY_SECRET_NAME' } // Example for secret handling + }, + build: { + dockerfile: './Dockerfile' // Assumes a Dockerfile in the repo + } +}); +``` + +### `reverse_proxy.caddy(config: CaddyConfig): CaddyProxy` +(Implicitly used or explicitly configurable) Configures Caddy as the reverse proxy. Typically, Caddy is automatically configured when `domain` is specified in `app.git`. Explicit configuration might be for advanced scenarios. + +- **`config: CaddyConfig`**: + - `email?: string`: Email for Let's Encrypt SSL certificate registration. + - `extra_config?: string`: Additional raw Caddyfile snippets. +- **Returns**: A `CaddyProxy` object. + +**Note**: Caddy is often managed automatically. You might not need to call this directly unless customizing global Caddy settings. + +### `deploy(server: Server, ...resources: (GitApp | PostgresDb | CaddyProxy)[])` +Generates the `hosty-playbook.yaml` Ansible playbook based on the defined server and resources. + +- **`server: Server`**: The target server object obtained from `server()`. +- **`...resources`: (GitApp | PostgresDb | CaddyProxy)[]**: A list of application, database, or explicit proxy resources to deploy. +- **Effect**: Creates/overwrites `hosty-playbook.yaml` in the current directory. + +**Example:** +```typescript +deploy(myVPS, mainDb, myApi); +``` + +### `run(options?: { verbose?: boolean; vars?: Record })` +Executes the generated `hosty-playbook.yaml` using `ansible-playbook`. + +- **`options?`**: + - `verbose?: boolean`: (Optional) Run Ansible with increased verbosity (`-v`). + - `vars?: Record`: (Optional) Extra variables to pass to the Ansible playbook. +- **Effect**: Connects to the server via SSH and applies the playbook. + +**Example:** +```typescript +async function main() { + // ... define myVPS, mainDb, myApi ... + deploy(myVPS, mainDb, myApi); + await run({ verbose: true }); +} +main(); +``` + +For the most precise and up-to-date details on configuration options and types, refer to the source code in `src/types.ts`, `src/server.ts`, `src/services/`, and `src/reverse_proxy/`. + +## Secret Management + +Managing sensitive information like API keys, database passwords, and other credentials securely is crucial. Hosty provides a mechanism to reference secrets, which are then expected to be available in the execution environment. + +When defining an application's environment variables using `app.git`, you can specify a secret like so: + +```typescript +const myApi = app.git({ + // ... other config ... + env: { + API_KEY: { secret: 'MY_APP_API_KEY' }, + DB_PASSWORD: { secret: 'DATABASE_PASSWORD_SECRET' } + } +}); +``` + +Hosty itself doesn't store or encrypt secrets. Instead, it relies on the environment where `ansible-playbook` (either via `run()` or the GitHub Action) is executed to provide these secrets as environment variables. + +### Supplying Secrets + +1. **Local Execution (using `run()`):** + When running Hosty locally, the Ansible playbook (`hosty-playbook.yaml`) generated by `deploy()` will expect the secrets (e.g., `MY_APP_API_KEY`, `DATABASE_PASSWORD_SECRET` from the example above) to be available as environment variables in the shell where you execute `run()`. + + For example, before running your Hosty script: + ```bash + export MY_APP_API_KEY="your_actual_api_key_value" + export DATABASE_PASSWORD_SECRET="your_db_password" + # Then run your node script that calls deploy() and run() + node deploy.ts + ``` + Alternatively, you can use tools like `direnv` or `.env` files (loaded by a library like `dotenv` in your deployment script *before* Hosty's `run()` is called) to manage these environment variables for local development. Hosty's `run()` function itself can also accept `vars` which can be sourced from environment variables. + +2. **GitHub Actions Execution:** + When using Hosty as a GitHub Action, you should store your secrets as [GitHub Encrypted Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in your repository settings (e.g., `Settings > Secrets and variables > Actions`). + + The `action.yml` for Hosty would then need to pass these GitHub secrets as environment variables to the `ansible-playbook` command. The `vars` input of the Hosty GitHub action can be used for this, or the action might be designed to automatically look for environment variables matching the secret names. + + For instance, if your Hosty script references `{ secret: 'MY_APP_API_KEY' }`, you would create a GitHub secret named `MY_APP_API_KEY`. The GitHub Action workflow would then need to make this available as an environment variable to the step running Hosty. + + Example snippet in a GitHub workflow: + ```yaml + - name: Run Hosty Deployment + uses: webNeat/hosty@v1 # Or your specific version + with: + # ... other inputs like server_ip, ssh_private_key ... + handler: 'deploy.ts' # Your Hosty script + env: + MY_APP_API_KEY: ${{ secrets.MY_APP_API_KEY }} + DATABASE_PASSWORD_SECRET: ${{ secrets.DATABASE_PASSWORD_SECRET }} + ``` + The Hosty action's internal script (`src/ci.ts`) would then ensure these environment variables are accessible when Ansible is run. + +### Best Practices: + +* **Never hardcode secrets** directly in your Hosty TypeScript files or commit them to version control. +* Use descriptive names for your secrets (e.g., `STRIPE_API_KEY` rather than just `API_KEY`). +* Limit the scope and permissions of secrets to the minimum required. +* Regularly rotate secrets. + +## Troubleshooting & Debugging + +Deployments can sometimes run into issues. Here are common areas to check and tips for debugging Hosty deployments: + +### 1. SSH Connection Issues + +* **Verify SSH Access**: Ensure you can manually SSH into the target server from the machine running Hosty (or from the GitHub Actions runner environment if applicable) using the specified user and SSH key. + ```bash + ssh -i /path/to/your/private_key user@server_ip + ``` +* **SSH Key Format & Permissions**: Ensure your SSH private key is in the correct format (usually OpenSSH) and has appropriate file permissions (e.g., `chmod 600 /path/to/your/private_key`). +* **`known_hosts`**: The Hosty GitHub Action attempts to add the server to `known_hosts`. Locally, you might need to do this manually upon first connection or ensure host key checking is handled if it's a new server. +* **Firewall**: Check if a firewall on the server or an intermediary network is blocking SSH connections (default port 22). + +### 2. Ansible Playbook Errors + +When `run()` or the GitHub Action executes `ansible-playbook`, errors might occur. + +* **Verbose Output**: + * Locally: Use `await run({ verbose: true });` in your Hosty script. + * GitHub Action: Set the `verbose: true` input for the Hosty action. + This will provide more detailed output from Ansible, often pinpointing the exact task that failed and the reason. +* **Inspect `hosty-playbook.yaml`**: After `deploy()` runs, this file is generated in your project root. Review its contents to understand the tasks Ansible is trying to execute. This can help you see if the generated playbook matches your expectations. +* **Common Ansible Errors**: + * *Package not found*: The package name might be incorrect for the server's OS, or the package repositories might need updating (`apt update`). + * *Service failing to start*: Check service logs on the server (e.g., `sudo systemctl status `, `sudo journalctl -u `). + * *Permission denied*: The SSH user might not have sufficient `sudo` privileges for a specific task, or file permissions on the server might be incorrect. + * *Template errors*: If Hosty uses Ansible templates, there might be syntax issues or missing variables in the template. + +### 3. Application Deployment Failures (e.g., Docker-based apps) + +If Ansible tasks complete but your application isn't working: + +* **Docker Logs**: If your application is containerized (common with `app.git`), check the Docker container logs on the server: + ```bash + sudo docker ps -a # List all containers, find your app's container ID or name + sudo docker logs + ``` +* **Docker Build Issues**: If Hosty builds a Docker image from a `Dockerfile`: + * Ensure the `Dockerfile` is correct and all necessary files are present in the build context. + * Verbose Ansible output might show Docker build errors. +* **Environment Variables**: Double-check that all required environment variables (including secrets) are correctly passed to the application container. You can inspect a running container's environment variables using `sudo docker inspect `. +* **Port Conflicts**: Ensure the ports your application tries to use are not already in use on the server or by other Docker containers. +* **Application-Specific Logs**: Check any log files your application writes to within its container or mounted volumes. + +### 4. Caddy / Reverse Proxy Issues + +* **Caddy Logs**: Check Caddy's logs for errors related to domain validation, SSL certificate acquisition, or request proxying. + ```bash + # If Caddy is run as a systemd service + sudo systemctl status caddy + sudo journalctl -u caddy + # If Caddy is run via Docker + sudo docker logs + ``` +* **DNS Configuration**: Ensure your domain's DNS records are correctly pointing to the server's IP address. +* **Firewall**: Make sure ports 80 and 443 are open on the server's firewall to allow HTTP and HTTPS traffic for Caddy. + +### General Debugging Tips + +* **Idempotency**: Ansible is designed to be idempotent. Re-running `run()` should ideally bring the system to the desired state without adverse effects. If a run fails, you can often fix the issue and re-run it. +* **Simplify**: If you have a complex deployment, try commenting out resources in your Hosty script to deploy a minimal setup first. Gradually add resources back to isolate the problematic component. +* **Check Hosty's Source**: If you suspect an issue with Hosty itself or how it generates playbooks, looking at the relevant modules in `src/` (e.g., `src/instance.ts`, `src/services/`, `src/ansible/`) can provide insights. + ## Development Workflow 1. Implement or modify code in `src/` diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b333b0d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,7 @@ +services: + hosty: + build: . + privileged: true + volumes: + - .:/app + working_dir: /app diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 840e79a..d4a7c72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,58 +28,8 @@ importers: specifier: ^8.1.4 version: 8.1.5 - packages/hosty: - dependencies: - yaml: - specifier: ^2.4.5 - version: 2.5.1 - devDependencies: - '@types/node': - specifier: ^20.12.12 - version: 20.16.5 - prettier: - specifier: ^3.2.5 - version: 3.3.3 - tsx: - specifier: ^4.10.5 - version: 4.19.0 - typescript: - specifier: ^5.4.5 - version: 5.6.2 - zx: - specifier: ^8.1.4 - version: 8.1.5 - - packages/hosty-action: - dependencies: - '@actions/core': - specifier: ^1.10.1 - version: 1.10.1 - hosty: - specifier: workspace:* - version: link:../hosty - devDependencies: - '@types/node': - specifier: ^20.12.12 - version: 20.16.5 - '@vercel/ncc': - specifier: ^0.38.1 - version: 0.38.1 - typescript: - specifier: ^5.6.2 - version: 5.6.2 - zx: - specifier: ^8.1.4 - version: 8.1.5 - packages: - '@actions/core@1.10.1': - resolution: {integrity: sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==} - - '@actions/http-client@2.2.3': - resolution: {integrity: sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==} - '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -224,10 +174,6 @@ packages: cpu: [x64] os: [win32] - '@fastify/busboy@2.1.1': - resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} - engines: {node: '>=14'} - '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} @@ -237,10 +183,6 @@ packages: '@types/node@20.16.5': resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} - '@vercel/ncc@0.38.1': - resolution: {integrity: sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==} - hasBin: true - esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -267,10 +209,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - typescript@5.6.2: resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} @@ -279,14 +217,6 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici@5.28.4: - resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} - engines: {node: '>=14.0'} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - yaml@2.5.1: resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} @@ -299,16 +229,6 @@ packages: snapshots: - '@actions/core@1.10.1': - dependencies: - '@actions/http-client': 2.2.3 - uuid: 8.3.2 - - '@actions/http-client@2.2.3': - dependencies: - tunnel: 0.0.6 - undici: 5.28.4 - '@esbuild/aix-ppc64@0.23.1': optional: true @@ -381,8 +301,6 @@ snapshots: '@esbuild/win32-x64@0.23.1': optional: true - '@fastify/busboy@2.1.1': {} - '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 @@ -398,8 +316,6 @@ snapshots: dependencies: undici-types: 6.19.8 - '@vercel/ncc@0.38.1': {} - esbuild@0.23.1: optionalDependencies: '@esbuild/aix-ppc64': 0.23.1 @@ -445,18 +361,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - tunnel@0.0.6: {} - typescript@5.6.2: {} undici-types@6.19.8: {} - undici@5.28.4: - dependencies: - '@fastify/busboy': 2.1.1 - - uuid@8.3.2: {} - yaml@2.5.1: {} zx@8.1.5: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/ansible/tasks/builtin.ts b/src/ansible/tasks/builtin.ts index f311b9a..624b4f1 100644 --- a/src/ansible/tasks/builtin.ts +++ b/src/ansible/tasks/builtin.ts @@ -24,7 +24,7 @@ export function file(name: string, attrs: FileAttrs, common: CommonTaskAttrs = { return { name, 'ansible.builtin.file': attrs, ...common } } -export type LineInFileAttrs = { path: string; line: string; state: 'present' | 'absent' } +export type LineInFileAttrs = { path: string; line: string; state: 'present' | 'absent'; unsafe_writes?: boolean } export function lineinfile(name: string, attrs: LineInFileAttrs, common: CommonTaskAttrs = {}): Task<'ansible.builtin.lineinfile', LineInFileAttrs> { return { name, 'ansible.builtin.lineinfile': attrs, ...common } } diff --git a/src/blocks/create_caddy_domain.ts b/src/blocks/create_caddy_domain.ts index c578bcc..6716be4 100644 --- a/src/blocks/create_caddy_domain.ts +++ b/src/blocks/create_caddy_domain.ts @@ -12,7 +12,7 @@ export function create_caddy_domain(config: Config): Block { return block(`Configure Caddy domain: ${config.domain}`, {}, [ builtin.lineinfile( `Ensure ${config.domain} is in /etc/hosts`, - { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present' }, + { path: '/etc/hosts', line: `127.0.0.1 ${config.domain}`, state: 'present', unsafe_writes: true }, { become: true }, ), builtin.copy(`Create Caddyfile for ${config.domain}`, { dest: config.caddyfile_path, content: config.caddyfile_content }, { register: 'caddyfile' }), diff --git a/src/blocks/delete_domain.ts b/src/blocks/delete_domain.ts index 2d1794b..8b448a9 100644 --- a/src/blocks/delete_domain.ts +++ b/src/blocks/delete_domain.ts @@ -8,7 +8,11 @@ type Config = { export function delete_domain({ domain, caddyfile_path }: Config) { return block(`Delete domain ${domain}`, {}, [ - builtin.lineinfile(`Remove ${domain} from /etc/hosts`, { path: '/etc/hosts', line: `127.0.0.1 ${domain}`, state: 'absent' }, { become: true }), + builtin.lineinfile( + `Remove ${domain} from /etc/hosts`, + { path: '/etc/hosts', line: `127.0.0.1 ${domain}`, state: 'absent', unsafe_writes: true }, + { become: true }, + ), builtin.file(`Delete Caddyfile for ${domain}`, { path: caddyfile_path, state: 'absent' }, { register: 'caddyfile' }), builtin.command(`Reload caddy`, { cmd: `sudo systemctl reload caddy` }, { become: true, when: 'caddyfile.changed' }), ]).get() diff --git a/src/blocks/generate_ssh_key.ts b/src/blocks/generate_ssh_key.ts index 909179d..3a7e761 100644 --- a/src/blocks/generate_ssh_key.ts +++ b/src/blocks/generate_ssh_key.ts @@ -5,7 +5,7 @@ import { block } from './block.js' type Config = { path: string - passphrase: string + passphrase?: string } export function generate_ssh_key({ path, passphrase }: Config): Block { diff --git a/src/types.ts b/src/types.ts index 61969c9..5a7a851 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,7 +28,7 @@ export type ServerConfig = { name: string ssh_key?: { path: string - passphrase: string + passphrase?: string } git_config?: { name?: string; email?: string } hosty_dir?: string diff --git a/tests/utils/index.ts b/tests/utils/index.ts index 5f53536..e57917f 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -78,7 +78,7 @@ async function run_test_case({ name, fn }: TestCase) { const container = server({ name: 'localhost', connection: { type: 'local', user }, - ssh_key: { path: '~/.ssh/id_rsa', passphrase: '' }, + ssh_key: { path: '~/.ssh/id_rsa' }, git_config: { name: 'Amine Ben hammou', email: 'webneat@gmail.com' }, }) const test_instance = instance() From 69a2548d184fbc48ca530516c60e47844a01720b Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Wed, 28 May 2025 18:00:01 +0200 Subject: [PATCH 4/4] Run tests in parallel --- .github/workflows/tests.yaml | 2 +- Dockerfile | 36 +-- docs/upgrade.md | 83 ----- package.json | 15 +- pnpm-lock.yaml | 302 +++++++++--------- src/ansible/tasks/builtin.ts | 2 +- src/blocks/build_repo.ts | 2 +- test.ts | 90 +++++- tests/app-adonis-sqlite.test.ts | 2 +- tests/app-laravel-mysql-custom-docker.test.ts | 2 +- tests/app-node-postgres.test.ts | 2 +- tests/app-rust-nextjs.test.ts | 2 +- tests/command-cron-host.test.ts | 2 +- tests/command-cron-service.test.ts | 2 +- tests/container.test.ts | 2 +- tests/setup.test.ts | 2 +- tests/{utils/index.ts => utils.ts} | 16 +- tests/utils/Dockerfile | 17 - 18 files changed, 260 insertions(+), 321 deletions(-) delete mode 100644 docs/upgrade.md rename tests/{utils/index.ts => utils.ts} (93%) delete mode 100644 tests/utils/Dockerfile diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 931af80..6eb95d4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -32,4 +32,4 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build - - run: pnpm run test ${{matrix.test-file}} \ No newline at end of file + - run: pnpm run ci:test ${{matrix.test-file}} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 00e1992..f0e30cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,11 +2,9 @@ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - ca-certificates curl gnupg lsb-release && \ + apt-get install -y --no-install-recommends ca-certificates curl gnupg lsb-release cron && \ mkdir -p -m 0755 /etc/apt/keyrings && \ - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ - gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ > /etc/apt/sources.list.d/docker.list && \ @@ -14,39 +12,13 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-buildx-plugin && \ apt-get clean && rm -rf /var/lib/apt/lists/* -# Install Nixpacks -RUN curl -sSL https://nixpacks.com/install.sh | bash && \ - apt-get clean && rm -rf /var/lib/apt/lists/* - -# Install Hosty test dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends ca-certificates curl gnupg && \ curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ - apt-get install -y --no-install-recommends \ - nodejs \ - python3 \ - python3-pip \ - ansible \ - git \ - sudo \ - openssh-client && \ + apt-get install -y --no-install-recommends git nodejs python3 python3-pip python3-apt ansible sudo openssh-client && \ npm install -g pnpm && \ + curl -sSL https://nixpacks.com/install.sh | bash && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'set -e' \ - '# start daemon in background, redirecting logs' \ - 'dockerd --host=unix:///var/run/docker.sock --data-root=/var/lib/docker > /var/log/dockerd.log 2>&1 &' \ - '# wait until it is ready' \ - 'until docker info >/dev/null 2>&1; do' \ - ' echo "Waiting for Docker daemon to start... (logs in /var/log/dockerd.log)"' \ - ' sleep 0.2' \ - 'done' \ - 'exec "$@"' \ - > /usr/local/bin/dind-entrypoint && chmod +x /usr/local/bin/dind-entrypoint - VOLUME /var/lib/docker -ENTRYPOINT ["/usr/local/bin/dind-entrypoint"] -CMD ["bash"] diff --git a/docs/upgrade.md b/docs/upgrade.md deleted file mode 100644 index 07b0d21..0000000 --- a/docs/upgrade.md +++ /dev/null @@ -1,83 +0,0 @@ -# Upgrade Guide: Migrating to Container-based Caddy - -This guide walks you through migrating from system-level Caddy to the Docker-based Caddy setup. - -## 1. Copy system Caddy config - -Before uninstalling, copy your existing Caddy config into the Hosty directory: - -```bash -sudo mkdir -p /srv/hosty/caddy/config /srv/hosty/caddy/includes /srv/hosty/caddy/logs -sudo cp /etc/caddy/Caddyfile /srv/hosty/caddy/config/ -sudo cp -r /etc/caddy/certs /srv/hosty/caddy/config/ -sudo cp /etc/caddy/includes/*.Caddyfile /srv/hosty/caddy/includes/ -``` - -## 2. Stop and Remove System Caddy - -Run: - -```bash -sudo systemctl stop caddy -sudo apt remove --purge caddy -``` - -Remove residual files: - -```bash -sudo rm -rf /etc/caddy /var/log/caddy -``` - -This stops and uninstalls the legacy system‑level Caddy package. - -## 3. Update DNS or /etc/hosts - -Ensure your domains point to the host machine: - -```bash -# Example for a local test domain -echo "127.0.0.1 example.local" | sudo tee -a /etc/hosts -``` - -## 4. Deploy Container-based Caddy - -Re-run your Hosty script: - -```bash -deploy(vps, /* your services */) -run({ ask_sudo_pass: true }) -``` - -This will start the Caddy Docker container, load your existing config, and serve your sites with Docker-managed TLS. - -## 5. Automate Migration with Hosty - -You can automate the migration via Hosty's `tasks` helper: - -```ts -import path from 'path' -import { server, tasks, deploy, run, blocks, ansible } from 'hosty' -const { builtin } = ansible.tasks - -const vps = server({ name: 'your-server-ip-or-hostname' }) -const caddy_dir = path.join(vps.hosty_dir, 'caddy') -deploy( - vps, - tasks( - blocks.create_directory(caddy_dir), - blocks.create_directory(path.join(caddy_dir, 'config')), - blocks.create_directory(path.join(caddy_dir, 'includes')), - blocks.create_directory(path.join(caddy_dir, 'logs')), - builtin.copy('Copy Caddyfile', { src: '/etc/caddy/Caddyfile', dest: path.join(caddy_dir, 'config/Caddyfile') }), - builtin.copy('Copy certificates', { src: '/etc/caddy/certs', dest: path.join(caddy_dir, 'config/certs'), recursive: true }), - builtin.copy('Copy includes', { src: '/etc/caddy/includes', dest: path.join(caddy_dir, 'includes'), recursive: true }), - builtin.command('Stop and remove system Caddy', { cmd: 'sudo systemctl stop caddy && sudo apt remove --purge caddy' }, { become: true }), - blocks.delete_directory(path.join(caddy_dir, 'config')), - blocks.delete_directory(path.join(caddy_dir, 'includes')), - blocks.delete_directory(path.join(caddy_dir, 'logs')), - ) -) -run({ ask_sudo_pass: true }) -``` - -That's it! Your migration is fully automated and your setup is containerized under Hosty. diff --git a/package.json b/package.json index 473f259..2a914a8 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ ], "scripts": { "build": "tsc -p tsconfig.build.json", - "test": "node --import tsx test.ts" + "test": "rm -rf .tests/* && docker builder prune -f -a && node --import tsx test.ts", + "ci:test": "NODE_ENV=ci node --import tsx test.ts" }, "repository": { "type": "git", @@ -30,13 +31,13 @@ }, "homepage": "https://github.com/webNeat/hosty", "devDependencies": { - "@types/node": "^20.16.5", - "prettier": "^3.2.5", - "tsx": "^4.10.5", - "typescript": "^5.4.5", - "zx": "^8.1.4" + "@types/node": "^24.3.0", + "prettier": "^3.6.2", + "tsx": "^4.20.4", + "typescript": "^5.9.2", + "zx": "^8.8.0" }, "dependencies": { - "yaml": "^2.4.5" + "yaml": "^2.8.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4a7c72..e553018 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,182 +9,188 @@ importers: .: dependencies: yaml: - specifier: ^2.4.5 - version: 2.5.1 + specifier: ^2.8.1 + version: 2.8.1 devDependencies: '@types/node': - specifier: ^20.16.5 - version: 20.16.5 + specifier: ^24.3.0 + version: 24.3.0 prettier: - specifier: ^3.2.5 - version: 3.3.3 + specifier: ^3.6.2 + version: 3.6.2 tsx: - specifier: ^4.10.5 - version: 4.19.0 + specifier: ^4.20.4 + version: 4.20.4 typescript: - specifier: ^5.4.5 - version: 5.6.2 + specifier: ^5.9.2 + version: 5.9.2 zx: - specifier: ^8.1.4 - version: 8.1.5 + specifier: ^8.8.0 + version: 8.8.0 packages: - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@types/fs-extra@11.0.4': - resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - - '@types/jsonfile@6.1.4': - resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - - '@types/node@20.16.5': - resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} + '@types/node@24.3.0': + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -196,152 +202,149 @@ packages: get-tsconfig@4.8.0: resolution: {integrity: sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==} - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} hasBin: true resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - tsx@4.19.0: - resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==} + tsx@4.20.4: + resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==} engines: {node: '>=18.0.0'} hasBin: true - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} - yaml@2.5.1: - resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} - engines: {node: '>= 14'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} hasBin: true - zx@8.1.5: - resolution: {integrity: sha512-gvmiYPvDDEz2Gcc37x7pJkipTKcFIE18q9QlSI1p5qoPDtoSn3jmGuWD0eEb7HuxEH5aDD7N/RVgH8BqSxbKzA==} + zx@8.8.0: + resolution: {integrity: sha512-v0VZXgSHusDvTtZROno3Ws8xkE1uNSSwH/yF8Fm+ZwBrYhr+bRNNpsnTJ32eR/t6umc7lAz5WqdP800ugW9zFA==} engines: {node: '>= 12.17.0'} hasBin: true snapshots: - '@esbuild/aix-ppc64@0.23.1': + '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/android-arm64@0.23.1': + '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/android-arm@0.23.1': + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-x64@0.23.1': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.23.1': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.23.1': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.23.1': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.23.1': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/linux-arm64@0.23.1': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm@0.23.1': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-ia32@0.23.1': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-loong64@0.23.1': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.23.1': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-ppc64@0.23.1': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.23.1': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-s390x@0.23.1': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-x64@0.23.1': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.23.1': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.23.1': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.23.1': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/sunos-x64@0.23.1': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/win32-arm64@0.23.1': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/win32-ia32@0.23.1': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/win32-x64@0.23.1': + '@esbuild/win32-arm64@0.25.9': optional: true - '@types/fs-extra@11.0.4': - dependencies: - '@types/jsonfile': 6.1.4 - '@types/node': 20.16.5 + '@esbuild/win32-ia32@0.25.9': optional: true - '@types/jsonfile@6.1.4': - dependencies: - '@types/node': 20.16.5 + '@esbuild/win32-x64@0.25.9': optional: true - '@types/node@20.16.5': + '@types/node@24.3.0': dependencies: - undici-types: 6.19.8 + undici-types: 7.10.0 - esbuild@0.23.1: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 fsevents@2.3.3: optional: true @@ -350,24 +353,21 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - prettier@3.3.3: {} + prettier@3.6.2: {} resolve-pkg-maps@1.0.0: {} - tsx@4.19.0: + tsx@4.20.4: dependencies: - esbuild: 0.23.1 + esbuild: 0.25.9 get-tsconfig: 4.8.0 optionalDependencies: fsevents: 2.3.3 - typescript@5.6.2: {} + typescript@5.9.2: {} - undici-types@6.19.8: {} + undici-types@7.10.0: {} - yaml@2.5.1: {} + yaml@2.8.1: {} - zx@8.1.5: - optionalDependencies: - '@types/fs-extra': 11.0.4 - '@types/node': 20.16.5 + zx@8.8.0: {} diff --git a/src/ansible/tasks/builtin.ts b/src/ansible/tasks/builtin.ts index 624b4f1..a3d5fbb 100644 --- a/src/ansible/tasks/builtin.ts +++ b/src/ansible/tasks/builtin.ts @@ -49,7 +49,7 @@ export function get_url(name: string, attrs: GetUrlAttrs, common: CommonTaskAttr return { name, 'ansible.builtin.get_url': attrs, ...common } } -export type GitAttrs = { repo: string; dest: string; version: string; accept_hostkey: boolean } +export type GitAttrs = { repo: string; dest: string; version: string; accept_hostkey: boolean; depth?: number } export function git(name: string, attrs: GitAttrs, common: CommonTaskAttrs = {}): Task<'ansible.builtin.git', GitAttrs> { return { name, 'ansible.builtin.git': attrs, ...common } } diff --git a/src/blocks/build_repo.ts b/src/blocks/build_repo.ts index 65f4901..bedb845 100644 --- a/src/blocks/build_repo.ts +++ b/src/blocks/build_repo.ts @@ -30,7 +30,7 @@ export function build_repo(config: Config): Block { builtin.tempfile(`Create a temp dir to clone the repo`, { state: 'directory' }, { register: 'clone_dir', when: 'source_file.changed' }), builtin.git( `Clone the repo`, - { repo: config.repo_url, version: config.branch, accept_hostkey: true, dest: '{{clone_dir.path}}' }, + { repo: config.repo_url, version: config.branch, accept_hostkey: true, dest: '{{clone_dir.path}}', depth: 1 }, { when: 'source_file.changed' }, ), builtin.copy( diff --git a/test.ts b/test.ts index cf8fa90..84cf094 100644 --- a/test.ts +++ b/test.ts @@ -1,17 +1,83 @@ -import { glob } from 'zx' -import { run } from './tests/utils/index.js' +import { chmod, mkdir, writeFile } from 'fs/promises' +import path from 'path' +import { Readable } from 'stream' +import { setTimeout } from 'timers/promises' -let filenames = process.argv.slice(2) -filenames = filenames.map((filename) => { - if (!filename.startsWith('./')) filename = './' + filename - return filename -}) -if (filenames.length === 0) { - filenames = await glob('./tests/*.test.ts') +const concurrency = 10 + +type Failure = { + filename: string + stdout: string + stderr: string +} + +const filenames = await get_test_files() +if (process.env.NODE_ENV === 'ci') { + await ci_run_tests(filenames) +} else { + const failures: Array = [] + await Readable.from(filenames).forEach((filename) => docker_run_test(failures, filename), { concurrency }) + if (failures.length > 0) { + for (const failure of failures) { + await writeFile(`.tests/${failure.filename}-stdout.log`, failure.stdout) + await writeFile(`.tests/${failure.filename}-stderr.log`, failure.stderr) + } + console.error(`❌ ${failures.length} tests failed, see .tests directory for logs`) + process.exit(1) + } +} + +async function get_test_files() { + let filenames = process.argv.slice(2) + filenames = filenames.map((filename) => { + if (!filename.startsWith('./')) filename = './' + filename + return filename + }) + if (filenames.length === 0) { + const { glob } = await import('zx') + filenames = await glob('./tests/*.test.ts') + } + return filenames +} + +async function ci_run_tests(filenames: string[]) { + const { run } = await import('./tests/utils.js') + for (const filename of filenames) { + await import(filename) + } + await run() } -for (const filename of filenames) { - await import(filename) +async function docker_run_test(failures: Failure[], filename: string) { + const { $ } = await import('zx') + const start = Date.now() + console.log(`⏳ ${filename}`) + const name = `hosty-test-${filename.slice(0, -8).replace(/[^a-z0-9A-Z]/g, '-')}` + const res = await docker_exec(name, filename) + const duration = Math.floor((Date.now() - start) / 1000) + if (res.exitCode !== 0) { + console.error(`❌ ${filename} (${duration}s)`) + failures.push({ + filename: name, + stdout: res.stdout, + stderr: res.stderr, + }) + } else { + console.log(`✅ ${filename} (${duration}s)`) + } } -run() +async function docker_exec(name: string, filename: string) { + const { $ } = await import('zx') + const exists = await $`docker inspect ${name}`.nothrow() + if (exists.exitCode !== 0) { + await $`docker compose run -d --name ${name} hosty dockerd --host=unix:///var/run/docker.sock --data-root=/var/lib/docker`.nothrow() + await setTimeout(10_000) + } + const running = await $`docker inspect -f {{.State.Running}} ${name}`.nothrow() + if (running.exitCode !== 0 || running.stdout.trim() !== 'true') { + await $`docker start ${name}`.nothrow() + await setTimeout(5_000) + } + return $`docker exec ${name} pnpm ci:test ${filename}`.nothrow() +} diff --git a/tests/app-adonis-sqlite.test.ts b/tests/app-adonis-sqlite.test.ts index 95bfe91..f24b5a0 100644 --- a/tests/app-adonis-sqlite.test.ts +++ b/tests/app-adonis-sqlite.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { app, command } from '../src/index.js' test('app: adonis + migrations + custom dockerfile', async ({ deploy, destroy, assert }) => { diff --git a/tests/app-laravel-mysql-custom-docker.test.ts b/tests/app-laravel-mysql-custom-docker.test.ts index f3ae210..e071ab7 100644 --- a/tests/app-laravel-mysql-custom-docker.test.ts +++ b/tests/app-laravel-mysql-custom-docker.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { app, db } from '../src/index.js' test('app: laravel + mysql + custom dockerfile', async ({ deploy, destroy, assert }) => { diff --git a/tests/app-node-postgres.test.ts b/tests/app-node-postgres.test.ts index cd6e135..6708248 100644 --- a/tests/app-node-postgres.test.ts +++ b/tests/app-node-postgres.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { app, db } from '../src/index.js' test('app: express + postgres', async ({ deploy, destroy, assert }) => { diff --git a/tests/app-rust-nextjs.test.ts b/tests/app-rust-nextjs.test.ts index e1aa9aa..6863248 100644 --- a/tests/app-rust-nextjs.test.ts +++ b/tests/app-rust-nextjs.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { app } from '../src/index.js' test('app: monorepo rust + nextjs', async ({ deploy, destroy, assert }) => { diff --git a/tests/command-cron-host.test.ts b/tests/command-cron-host.test.ts index 33bf899..bc1cf06 100644 --- a/tests/command-cron-host.test.ts +++ b/tests/command-cron-host.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { command } from '../src/index.js' test('add/delete cron to/from the server', async ({ deploy, destroy, assert }) => { diff --git a/tests/command-cron-service.test.ts b/tests/command-cron-service.test.ts index 092176b..52f0478 100644 --- a/tests/command-cron-service.test.ts +++ b/tests/command-cron-service.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' import { db, command } from '../src/index.js' test('add/delete cron to/from a service', async ({ deploy, destroy, assert }) => { diff --git a/tests/container.test.ts b/tests/container.test.ts index 0454145..98b5e3e 100644 --- a/tests/container.test.ts +++ b/tests/container.test.ts @@ -1,5 +1,5 @@ import { readFile } from 'fs/promises' -import { test } from './utils/index.js' +import { test } from './utils.js' import { container } from '../src/index.js' test('simple container', async ({ deploy, destroy, assert }) => { diff --git a/tests/setup.test.ts b/tests/setup.test.ts index c7c43b7..5ee2c63 100644 --- a/tests/setup.test.ts +++ b/tests/setup.test.ts @@ -1,4 +1,4 @@ -import { test } from './utils/index.js' +import { test } from './utils.js' test('setup', async ({ assert }) => { assert.command(`docker --version`, { success: true }) diff --git a/tests/utils/index.ts b/tests/utils.ts similarity index 93% rename from tests/utils/index.ts rename to tests/utils.ts index e57917f..1c5ba49 100644 --- a/tests/utils/index.ts +++ b/tests/utils.ts @@ -1,6 +1,6 @@ import * as zx from 'zx' import { ChildProcess } from 'child_process' -import { HostyInstance, Server, Service, tasks, server, internals } from '../../src/index.js' +import { HostyInstance, Server, Service, tasks, server, internals } from '../src/index.js' const { instance, @@ -54,11 +54,6 @@ export async function run() { } } - if (failures.length === 0) { - await $`rm -rf .tests` - return - } - for (const { name, output } of failures) { console.log('') console.log(`------------------------------------------`) @@ -67,11 +62,13 @@ export async function run() { console.log(output) } - process.exit(1) + if (failures.length > 0) { + process.exit(1) + } } async function run_test_case({ name, fn }: TestCase) { - const test_name = name.replace(/[^a-zA-Z0-9]/g, '-') + const test_name = name.replace(/[^a-zA-Z0-9]/g, '-') + Date.now() const playbook_path = `.tests/${test_name}.yaml` const user = (await $`whoami`).stdout.trim() @@ -85,6 +82,7 @@ async function run_test_case({ name, fn }: TestCase) { await fn(make_test_context(container, test_instance)) const res = await wait_process(await test_instance.run({ playbook_path, ask_sudo_pass: false, spawn_options: { stdio: 'pipe' } })) + await $`chmod 777 ${playbook_path}` if (res.exitCode) throw res.output await $`docker ps -q --filter "name=^/hosty-" | xargs -r docker stop` await $`docker ps -aq --filter "name=^/hosty-" | xargs -r docker rm` @@ -99,9 +97,11 @@ async function wait_process(ps: ChildProcess) { return new Promise((resolve, reject) => { const res: ProcessResult = { exitCode: null, output: '' } ps.stdout?.on('data', (data) => { + // console.error(data.toString()) res.output += data.toString() }) ps.stderr?.on('data', (data) => { + // console.error(data.toString()) res.output += data.toString() }) ps.on('close', (code) => { diff --git a/tests/utils/Dockerfile b/tests/utils/Dockerfile deleted file mode 100644 index 23ed94e..0000000 --- a/tests/utils/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM ubuntu:22.04 -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt update && apt install -y sudo -RUN useradd -m -s /bin/bash foo && echo 'foo:foo' | chpasswd -RUN usermod -aG sudo foo -RUN echo 'foo ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/foo && chmod 0440 /etc/sudoers.d/foo - -USER foo -RUN sudo apt update && sudo DEBIAN_FRONTEND=noninteractive apt install -y wget git -RUN wget https://get.docker.com -O /tmp/get-docker.sh && chmod +x /tmp/get-docker.sh && /tmp/get-docker.sh -RUN sudo usermod -aG docker foo -ENV PATH="/home/foo/.local/bin:$PATH" - -WORKDIR /home/foo - -CMD ["tail", "-f", "/dev/null"]