Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Kamal deployment #1192

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f8b6f48
feat: add Kamal deployment support
yinho999 Jan 19, 2025
bcd0f87
Merge branch 'master' into kamal-deployment
yinho999 Jan 19, 2025
fbb3502
Merge branch 'master' into kamal-deployment
yinho999 Jan 19, 2025
e6bdeb0
Added test for postgres, sqlite without/with background queue with redis
yinho999 Jan 19, 2025
af50d5f
Added binding 0.0.0.0 to development.yaml
yinho999 Jan 19, 2025
328ccae
removed duplicated dockerfile
yinho999 Jan 20, 2025
60fca18
Merge branch 'master' into kamal-deployment
yinho999 Jan 25, 2025
db6cdfd
feat: add .dockerignore and improve deployment file generation
yinho999 Jan 25, 2025
04a1c4c
style: reorganize imports across multiple files
yinho999 Jan 25, 2025
a75bd59
style: reformat doc comments and reorganize imports
yinho999 Jan 25, 2025
2ec6d2a
feat: backup existing deployment files before generation
yinho999 Jan 26, 2025
e04c8b5
feat: add default development.yaml config to deployment tests
yinho999 Jan 26, 2025
efbd6cc
Merge branch 'master' into kamal-deployment
yinho999 Jan 26, 2025
e124196
style: improve code formatting and readability
yinho999 Jan 26, 2025
b974128
refactor: extract scaffold kind determination into separate function
yinho999 Jan 26, 2025
8b3c713
feat: add Kamal deployment support
yinho999 Jan 26, 2025
c35d27f
Merge branch 'master' into kamal-deployment
yinho999 Jan 26, 2025
5852120
refactor: update server binding configuration in deployment templates
yinho999 Jan 26, 2025
2604d33
Merge remote-tracking branch 'origin/kamal-deployment' into kamal-dep…
yinho999 Jan 26, 2025
52240c7
Merge branch 'master' into kamal-deployment
yinho999 Jan 27, 2025
0f98eb6
Merge branch 'master' into kamal-deployment
yinho999 Feb 1, 2025
c9bb3e6
refactor: restructure Kamal deployment configuration and improve code…
yinho999 Feb 1, 2025
f87081d
style: improve code formatting and organization
yinho999 Feb 1, 2025
6b9ead9
test: add Kamal deployment generation test
yinho999 Feb 1, 2025
d2767af
style: improve code organization and line wrapping
yinho999 Feb 1, 2025
c6fb232
fix: update Kamal secrets directory path to use dot prefix
yinho999 Feb 2, 2025
64f02e7
feat: add Kamal deployment support for Rails applications
yinho999 Feb 2, 2025
8eb8897
Merge branch 'master' into kamal-deployment
yinho999 Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions loco-gen/src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use super::{AppInfo, GenerateResults, Result};
use crate as gen;
use std::path::Path;

use rrgen::RRgen;
use serde_json::json;
use std::path::Path;

use super::{AppInfo, GenerateResults, Result};
use crate as gen;

pub fn generate(
rrgen: &RRgen,
Expand Down
48 changes: 46 additions & 2 deletions loco-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ pub use rrgen::{GenResult, RRgen};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
mod controller;
use colored::Colorize;
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::OnceLock,
};

use colored::Colorize;

#[cfg(feature = "with-db")]
mod infer;
#[cfg(feature = "with-db")]
Expand Down Expand Up @@ -234,6 +235,13 @@ pub enum DeploymentKind {
host: String,
port: i32,
},
Kamal {
copy_paths: Vec<PathBuf>,
is_client_side_rendering: bool,
postgres: bool,
sqlite: bool,
background_queue: bool,
},
}

#[derive(Debug)]
Expand Down Expand Up @@ -382,6 +390,41 @@ pub fn generate(rrgen: &RRgen, component: Component, appinfo: &AppInfo) -> Resul
});
render_template(rrgen, Path::new("deployment/nginx"), &vars)?
}
DeploymentKind::Kamal {
copy_paths,
is_client_side_rendering,
postgres,
sqlite,
background_queue,
} => {
let vars = json!({
"pkg_name": appinfo.app_name,
"copy_paths": copy_paths,
"is_client_side_rendering": is_client_side_rendering,
"sqlite": sqlite,
"postgres": postgres,
"background_queue": background_queue
});
let config_deploy_yml = Path::new("config/deploy.yml");
let kamal_secrets = Path::new(".kamal/secrets");
if config_deploy_yml.exists() {
tracing::info!("backing up config/deploy.yml to config/_deploy.yml");
fs::rename(config_deploy_yml, Path::new("config/_deploy.yml"))?;
}
if kamal_secrets.exists() {
tracing::info!("backing up kamal/secrets to kamal/_secrets");
fs::rename(kamal_secrets, Path::new(".kamal/_secrets"))?;
}
// render the dockerfile template
let mut gen_result_docker =
render_template(rrgen, Path::new("deployment/docker"), &vars)?;
// render the kamal template
let gen_result_kamal =
render_template(rrgen, Path::new("deployment/kamal"), &vars)?;
// merge the results
gen_result_docker.rrgen.extend(gen_result_kamal.rrgen);
gen_result_docker
}
},
};

Expand Down Expand Up @@ -503,9 +546,10 @@ pub fn copy_template(path: &Path, to: &Path) -> Result<Vec<PathBuf>> {

#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;

use super::*;

#[test]
fn test_template_not_found() {
let tree_fs = tree_fs::TreeBuilder::default()
Expand Down
11 changes: 6 additions & 5 deletions loco-gen/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ pub fn get_columns_and_references(
let array_kind = match params.as_slice() {
[array_kind] => Ok(array_kind),
_ => Err(Error::Message(format!(
"type: `{ftype}` requires exactly {arity} parameter{}, but {} were given (`{}`).",
if arity == 1 { "" } else { "s" },
params.len(),
params.join(",")
))),
"type: `{ftype}` requires exactly {arity} parameter{}, but {} \
were given (`{}`).",
if arity == 1 { "" } else { "s" },
params.len(),
params.join(",")
))),
}?;

format!(
Expand Down
19 changes: 14 additions & 5 deletions loco-gen/src/template.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::{Error, Result};
use include_dir::{include_dir, Dir, DirEntry, File};
use std::path::{Path, PathBuf};

use include_dir::{include_dir, Dir, DirEntry, File};

use crate::{Error, Result};

static TEMPLATES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/templates");
pub const DEFAULT_LOCAL_TEMPLATE: &str = ".loco-templates";

Expand All @@ -24,7 +26,8 @@ pub fn exists(path: &Path) -> bool {
TEMPLATES.get_entry(path).is_some()
}

/// Determines whether a given path should be ignored based on the ignored paths list.
/// Determines whether a given path should be ignored based on the ignored paths
/// list.
#[must_use]
fn is_path_ignored(path: &Path, ignored_paths: &[&Path]) -> bool {
ignored_paths
Expand Down Expand Up @@ -80,7 +83,8 @@ pub fn collect_files_from_path(path: &Path) -> Result<Vec<&File<'_>>> {
)
}

/// Recursively collects all file paths from a directory, skipping ignored paths.
/// Recursively collects all file paths from a directory, skipping ignored
/// paths.
fn collect_files_path_recursively(dir: &Dir<'_>) -> Vec<PathBuf> {
let mut file_paths = Vec::new();

Expand Down Expand Up @@ -116,12 +120,17 @@ fn collect_files_recursively<'a>(dir: &'a Dir<'a>) -> Vec<&'a File<'a>> {

#[cfg(test)]
pub mod tests {
use super::*;
use std::path::Path;

use super::*;

/// Returns the first directory in the included templates.
/// # Panics
#[must_use]
pub fn find_first_dir() -> &'static Dir<'static> {
TEMPLATES.dirs().next().expect("first folder")
}
#[must_use]
pub fn find_first_file<'a>(dir: &'a Dir<'a>) -> Option<&'a File<'a>> {
for entry in dir.entries() {
match entry {
Expand Down
15 changes: 13 additions & 2 deletions loco-gen/src/templates/deployment/docker/docker.t
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
to: "dockerfile"
skip_exists: true
message: "Dockerfile generated successfully."

injections:
- into: config/development.yaml
remove_lines: |
# Binding for the server (which interface to bind to)
binding: {{ get_env(name="BINDING", default="localhost") }}
content: |
| # Binding for the server (which interface to bind to)
| binding: {{ get_env(name="BINDING", default="0.0.0.0") }}

---
FROM rust:1.83.0-slim as builder

FROM rust:1.84-slim as builder

WORKDIR /usr/src/

Expand Down Expand Up @@ -31,4 +42,4 @@ COPY --from=builder /usr/src/{{path}} {{path}}
COPY --from=builder /usr/src/config config
COPY --from=builder /usr/src/target/release/{{pkg_name}}-cli {{pkg_name}}-cli

ENTRYPOINT ["/usr/app/{{pkg_name}}-cli"]
ENTRYPOINT ["/usr/app/{{pkg_name}}-cli","start"]
154 changes: 154 additions & 0 deletions loco-gen/src/templates/deployment/kamal/deploy.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
to: "config/deploy.yml"
skip_exists: true
message: "Deploy file generated successfully."
---

# Name of your application. Used to uniquely configure containers.
service: {{pkg_name}}

# Name of the container image.
image: docker_username/{{pkg_name}}

# Deploy to these servers.
servers:
web:
- server_ip_address
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
proxy:
ssl: true
host: domain_name
# Proxy connects to your container on port 80 by default.
app_port: 5150
healthcheck:
interval: 3
path: /_health
timeout: 3

# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: docker_username

# Always use an access token rather than real password (pulled from .kamal/secrets).
password:
- KAMAL_REGISTRY_PASSWORD

# Configure builder setup.
builder:
arch: amd64
# Pass in additional build args needed for your Dockerfile.
# args:
dockerfile: dockerfile
# Inject ENV variables into containers (secrets come from .kamal/secrets).
#
# env:
# clear:
# DB_HOST: 192.168.0.2
# secret:
# - RAILS_MASTER_KEY
{% if postgres or background_queue %}
env:
clear:
{% endif -%}
{%- if background_queue %}
REDIS_URL: "redis://{{pkg_name}}-redis"
{%- endif %}
{%- if postgres %}
secret:
- DATABASE_URL
- POSTGRES_PASSWORD
{%- endif %}

# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
#
# aliases:
# shell: app exec --interactive --reuse "bash"

# Use a different ssh user than root
#
# ssh:
# user: app

# Use a persistent storage volume.
#
# volumes:
# - "app_storage:/app/storage"
{% if sqlite -%}
# Use a persistent database volume.
volumes:
# /var/lib/docker/volumes/data/_data
- "data:/usr/app"
{% endif -%}


# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# asset_path: /app/public/assets

# Configure rolling deploys by setting a wait time between batches of restarts.
#
# boot:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2

# Use accessory services (secrets come from .kamal/secrets).
#
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# port: 3306
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: valkey/valkey:8
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data

{% if postgres or background_queue %}
accessories:
{% endif -%}
{%- if postgres %}
db:
image: postgres:16
host: server_ip_address
port: "127.0.0.1:5432:5432"
env:
clear:
POSTGRES_USER: loco
POSTGRES_DB: {{pkg_name}}_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
{% endif %}
{%- if background_queue %}
redis:
image: valkey/valkey:8
host: server_ip_address
port: "127.0.0.1:6379:6379"
directories:
- data:/data
{% endif %}
26 changes: 26 additions & 0 deletions loco-gen/src/templates/deployment/kamal/secrets.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
to: ".kamal/secrets"
skip_exists: true
message: "Secrets file generated successfully."
---
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.

# Option 1: Read secrets from the environment
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
{% if postgres -%}
# example: export POSTGRES_PASSWORD="loco"
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
# example: export DATABASE_URL="postgresql://loco:$POSTGRES_PASSWORD@{{pkg_name}}-db:5432/{{pkg_name}}_production"
DATABASE_URL=$DATABASE_URL
{% endif %}
# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)

# Option 3: Read secrets via kamal secrets helpers
# These will handle logging in and fetching the secrets in as few calls as possible
# There are adapters for 1Password, LastPass + Bitwarden
#
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
Loading
Loading