propel_core/config.rs
1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Top-level `propel.toml` configuration.
6///
7/// All sections are optional — sensible defaults are provided.
8///
9/// # Example
10///
11/// ```toml
12/// [project]
13/// gcp_project_id = "my-project"
14///
15/// [build]
16/// include = ["migrations/", "templates/"]
17///
18/// [build.env]
19/// TEMPLATE_DIR = "/app/templates"
20///
21/// [cloud_run]
22/// memory = "1Gi"
23/// ```
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct PropelConfig {
26 #[serde(default)]
27 pub project: ProjectConfig,
28 #[serde(default)]
29 pub build: BuildConfig,
30 #[serde(default)]
31 pub cloud_run: CloudRunConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProjectConfig {
36 /// Project name (defaults to Cargo.toml package name)
37 pub name: Option<String>,
38 /// GCP region (defaults to us-central1)
39 #[serde(default = "default_region")]
40 pub region: String,
41 /// GCP project ID
42 pub gcp_project_id: Option<String>,
43}
44
45/// Build configuration under `[build]`.
46///
47/// Controls Docker image generation and runtime content.
48///
49/// # Bundle strategy
50///
51/// By default, `propel deploy` bundles **all files in the git repository**
52/// (respecting `.gitignore`) into the Docker build context. This mirrors
53/// the `git clone` + `docker build` mental model used by GitHub Actions
54/// and similar CI/CD systems.
55///
56/// The bundle is created via `git ls-files`, so:
57/// - Tracked and untracked (non-ignored) files are included
58/// - `.gitignore`d files (e.g. `target/`) are excluded
59/// - `.propel-bundle/`, `.propel/`, `.git/` are always excluded
60///
61/// # Runtime content control
62///
63/// The `include` field controls what goes into the **final runtime image**:
64///
65/// - **`include` omitted (default)**: the entire bundle is copied into the
66/// runtime container via `COPY . .`. Zero config — migrations, templates,
67/// static assets all work automatically.
68///
69/// - **`include` specified**: only the listed paths (plus the compiled binary)
70/// are copied into the runtime image. This acts as a lightweight alternative
71/// to `propel eject` for users who want smaller images.
72///
73/// # Escalation path
74///
75/// ```text
76/// Zero config → include/env → propel eject
77/// (all-in) (selective) (full Dockerfile control)
78/// ```
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct BuildConfig {
81 /// Rust builder image (default: `rust:1.84-bookworm`).
82 #[serde(default = "default_builder_image")]
83 pub base_image: String,
84 /// Runtime base image (default: `gcr.io/distroless/cc-debian12`).
85 #[serde(default = "default_runtime_image")]
86 pub runtime_image: String,
87 /// Additional system packages to install via `apt-get` during build.
88 #[serde(default)]
89 pub extra_packages: Vec<String>,
90 /// Cargo Chef version for dependency caching.
91 #[serde(default = "default_cargo_chef_version")]
92 pub cargo_chef_version: String,
93 /// Paths to copy into the runtime image.
94 ///
95 /// When `None`, the entire bundle is copied (`COPY . .`).
96 /// When `Some`, only these paths are copied — overriding the all-in default.
97 ///
98 /// ```toml
99 /// [build]
100 /// include = ["migrations/", "templates/"]
101 /// ```
102 #[serde(default)]
103 pub include: Option<Vec<String>>,
104 /// Static environment variables baked into the container image.
105 ///
106 /// These become `ENV` directives in the generated Dockerfile.
107 /// For runtime-configurable values (API keys, secrets), use
108 /// Cloud Run environment variables or Secret Manager instead.
109 ///
110 /// ```toml
111 /// [build.env]
112 /// TEMPLATE_DIR = "/app/templates"
113 /// ```
114 #[serde(default)]
115 pub env: HashMap<String, String>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CloudRunConfig {
120 /// Memory allocation
121 #[serde(default = "default_memory")]
122 pub memory: String,
123 /// CPU count
124 #[serde(default = "default_cpu")]
125 pub cpu: u32,
126 /// Minimum instances
127 #[serde(default)]
128 pub min_instances: u32,
129 /// Maximum instances
130 #[serde(default = "default_max_instances")]
131 pub max_instances: u32,
132 /// Max concurrent requests per instance
133 #[serde(default = "default_concurrency")]
134 pub concurrency: u32,
135 /// Port the application listens on
136 #[serde(default = "default_port")]
137 pub port: u16,
138}
139
140impl Default for ProjectConfig {
141 fn default() -> Self {
142 Self {
143 name: None,
144 region: default_region(),
145 gcp_project_id: None,
146 }
147 }
148}
149
150impl Default for BuildConfig {
151 fn default() -> Self {
152 Self {
153 base_image: default_builder_image(),
154 runtime_image: default_runtime_image(),
155 extra_packages: Vec::new(),
156 cargo_chef_version: default_cargo_chef_version(),
157 include: None,
158 env: HashMap::new(),
159 }
160 }
161}
162
163impl Default for CloudRunConfig {
164 fn default() -> Self {
165 Self {
166 memory: default_memory(),
167 cpu: default_cpu(),
168 min_instances: 0,
169 max_instances: default_max_instances(),
170 concurrency: default_concurrency(),
171 port: default_port(),
172 }
173 }
174}
175
176impl PropelConfig {
177 /// Load from propel.toml at the given path, or return defaults if not found.
178 pub fn load(project_dir: &std::path::Path) -> crate::Result<Self> {
179 let config_path = project_dir.join("propel.toml");
180 if config_path.exists() {
181 tracing::debug!(path = %config_path.display(), "loading propel.toml");
182 let content =
183 std::fs::read_to_string(&config_path).map_err(|e| crate::Error::ConfigLoad {
184 path: config_path.clone(),
185 source: e,
186 })?;
187 let config: Self = toml::from_str(&content).map_err(|e| crate::Error::ConfigParse {
188 path: config_path,
189 source: e,
190 })?;
191 tracing::debug!(
192 region = %config.project.region,
193 port = config.cloud_run.port,
194 "config loaded"
195 );
196 Ok(config)
197 } else {
198 tracing::debug!(path = %config_path.display(), "propel.toml not found, using defaults");
199 Ok(Self::default())
200 }
201 }
202}
203
204fn default_region() -> String {
205 "us-central1".to_owned()
206}
207
208fn default_builder_image() -> String {
209 "rust:1.84-bookworm".to_owned()
210}
211
212fn default_runtime_image() -> String {
213 "gcr.io/distroless/cc-debian12".to_owned()
214}
215
216fn default_cargo_chef_version() -> String {
217 "0.1.68".to_owned()
218}
219
220fn default_memory() -> String {
221 "512Mi".to_owned()
222}
223
224fn default_cpu() -> u32 {
225 1
226}
227
228fn default_max_instances() -> u32 {
229 10
230}
231
232fn default_concurrency() -> u32 {
233 80
234}
235
236fn default_port() -> u16 {
237 8080
238}