Skip to content

Commit

Permalink
Make the reverse_proxy configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
webNeat committed Jan 26, 2025
1 parent 8c16f87 commit 75635ee
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 59 deletions.
15 changes: 4 additions & 11 deletions src/blocks/create_domain.ts → src/blocks/create_caddy_domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
17 changes: 11 additions & 6 deletions src/blocks/create_directory.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
2 changes: 1 addition & 1 deletion src/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
17 changes: 7 additions & 10 deletions src/blocks/install_caddy.ts
Original file line number Diff line number Diff line change
@@ -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`,
Expand All @@ -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()
}
25 changes: 25 additions & 0 deletions src/files.ts
Original file line number Diff line number Diff line change
@@ -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 extends keyof FileData>(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
}
11 changes: 11 additions & 0 deletions src/files/server_caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
log {
format json
output file {{log_path}} {
roll_size 10mb
roll_keep 10
}
}
}

import {{service_caddyfiles_pattern}}
5 changes: 5 additions & 0 deletions src/files/service_caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{domain}} {
reverse_proxy {{local_urls}} {
lb_policy client_ip_hash
}
}
36 changes: 36 additions & 0 deletions src/reverse_proxy/caddy.ts
Original file line number Diff line number Diff line change
@@ -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<CaddyConfig> = {
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<CaddyConfig>, server: Server) {
return [blocks.install_caddy({ caddyfile_content: config.get_server_caddyfile(server) })]
}

function get_service_tasks(config: Required<CaddyConfig>, 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),
}),
]
}
1 change: 1 addition & 0 deletions src/reverse_proxy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './caddy.js'
8 changes: 7 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
}
Expand Down Expand Up @@ -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),
]
}
12 changes: 9 additions & 3 deletions src/services/app/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerConfig> & {
services_dir: string
backups_dir: string
logs_dir: string
get_service_dir: (name: string) => string
get_backups_dir: (name: string) => string
}
Expand Down
39 changes: 12 additions & 27 deletions tasks.todo
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions tests/app-laravel-mysql-custom-docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
})

0 comments on commit 75635ee

Please sign in to comment.