pingora_core/server/configuration/mod.rs
1// Copyright 2024 Cloudflare, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Server configurations
16//!
17//! Server configurations define startup settings such as:
18//! * User and group to run as after daemonization
19//! * Number of threads per service
20//! * Error log file path
21
22use clap::Parser;
23use log::{debug, trace};
24use pingora_error::{Error, ErrorType::*, OrErr, Result};
25use serde::{Deserialize, Serialize};
26use std::fs;
27
28/// The configuration file
29///
30/// Pingora configuration files are by default YAML files, but any key value format can potentially
31/// be used.
32///
33/// # Extension
34/// New keys can be added to the configuration files which this configuration object will ignore.
35/// Then, users can parse these key-values to pass to their code to use.
36#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(default)]
38pub struct ServerConf {
39 /// Version
40 pub version: usize,
41 /// Whether to run this process in the background.
42 pub daemon: bool,
43 /// When configured and `daemon` setting is `true`, error log will be written to the given
44 /// file. Otherwise StdErr will be used.
45 pub error_log: Option<String>,
46 /// The pid (process ID) file of this server to be created when running in background
47 pub pid_file: String,
48 /// the path to the upgrade socket
49 ///
50 /// In order to perform zero downtime restart, both the new and old process need to agree on the
51 /// path to this sock in order to coordinate the upgrade.
52 pub upgrade_sock: String,
53 /// If configured, after daemonization, this process will switch to the given user before
54 /// starting to serve traffic.
55 pub user: Option<String>,
56 /// Similar to `user`, the group this process should switch to.
57 pub group: Option<String>,
58 /// How many threads **each** service should get. The threads are not shared across services.
59 pub threads: usize,
60 /// Allow work stealing between threads of the same service. Default `true`.
61 pub work_stealing: bool,
62 /// The path to CA file the SSL library should use. If empty, the default trust store location
63 /// defined by the SSL library will be used.
64 pub ca_file: Option<String>,
65 /// Grace period in seconds before starting the final step of the graceful shutdown after signaling shutdown.
66 pub grace_period_seconds: Option<u64>,
67 /// Timeout in seconds of the final step for the graceful shutdown.
68 pub graceful_shutdown_timeout_seconds: Option<u64>,
69 // These options don't belong here as they are specific to certain services
70 /// IPv4 addresses for a client connector to bind to. See
71 /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
72 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
73 pub client_bind_to_ipv4: Vec<String>,
74 /// IPv6 addresses for a client connector to bind to. See
75 /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
76 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
77 pub client_bind_to_ipv6: Vec<String>,
78 /// Keepalive pool size for client connections to upstream. See
79 /// [`ConnectorOptions`](crate::connectors::ConnectorOptions).
80 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
81 pub upstream_keepalive_pool_size: usize,
82 /// Number of dedicated thread pools to use for upstream connection establishment.
83 /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
84 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
85 pub upstream_connect_offload_threadpools: Option<usize>,
86 /// Number of threads per dedicated upstream connection establishment pool.
87 /// See [`ConnectorOptions`](crate::connectors::ConnectorOptions).
88 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
89 pub upstream_connect_offload_thread_per_pool: Option<usize>,
90 /// When enabled allows TLS keys to be written to a file specified by the SSLKEYLOG
91 /// env variable. This can be used by tools like Wireshark to decrypt upstream traffic
92 /// for debugging purposes.
93 /// Note: this is an _unstable_ field that may be renamed or removed in the future.
94 pub upstream_debug_ssl_keylog: bool,
95}
96
97impl Default for ServerConf {
98 fn default() -> Self {
99 ServerConf {
100 version: 0,
101 client_bind_to_ipv4: vec![],
102 client_bind_to_ipv6: vec![],
103 ca_file: None,
104 daemon: false,
105 error_log: None,
106 upstream_debug_ssl_keylog: false,
107 pid_file: "/tmp/pingora.pid".to_string(),
108 upgrade_sock: "/tmp/pingora_upgrade.sock".to_string(),
109 user: None,
110 group: None,
111 threads: 1,
112 work_stealing: true,
113 upstream_keepalive_pool_size: 128,
114 upstream_connect_offload_threadpools: None,
115 upstream_connect_offload_thread_per_pool: None,
116 grace_period_seconds: None,
117 graceful_shutdown_timeout_seconds: None,
118 }
119 }
120}
121
122/// Command-line options
123///
124/// Call `Opt::parse_args()` to build this object from the process's command line arguments.
125#[derive(Parser, Debug, Default)]
126#[clap(name = "basic", long_about = None)]
127pub struct Opt {
128 /// Whether this server should try to upgrade from a running old server
129 #[clap(
130 short,
131 long,
132 help = "This is the base set of command line arguments for a pingora-based service",
133 long_help = None
134 )]
135 pub upgrade: bool,
136
137 /// Whether this server should run in the background
138 #[clap(short, long)]
139 pub daemon: bool,
140
141 /// Not actually used. This flag is there so that the server is not upset seeing this flag
142 /// passed from `cargo test` sometimes
143 #[clap(long, hidden = true)]
144 pub nocapture: bool,
145
146 /// Test the configuration and exit
147 ///
148 /// When this flag is set, calling `server.bootstrap()` will exit the process without errors
149 ///
150 /// This flag is useful for upgrading service where the user wants to make sure the new
151 /// service can start before shutting down the old server process.
152 #[clap(
153 short,
154 long,
155 help = "This flag is useful for upgrading service where the user wants \
156 to make sure the new service can start before shutting down \
157 the old server process.",
158 long_help = None
159 )]
160 pub test: bool,
161
162 /// The path to the configuration file.
163 ///
164 /// See [`ServerConf`] for more details of the configuration file.
165 #[clap(short, long, help = "The path to the configuration file.", long_help = None)]
166 pub conf: Option<String>,
167}
168
169impl ServerConf {
170 // Does not has to be async until we want runtime reload
171 pub fn load_from_yaml<P>(path: P) -> Result<Self>
172 where
173 P: AsRef<std::path::Path> + std::fmt::Display,
174 {
175 let conf_str = fs::read_to_string(&path).or_err_with(ReadError, || {
176 format!("Unable to read conf file from {path}")
177 })?;
178 debug!("Conf file read from {path}");
179 Self::from_yaml(&conf_str)
180 }
181
182 pub fn load_yaml_with_opt_override(opt: &Opt) -> Result<Self> {
183 if let Some(path) = &opt.conf {
184 let mut conf = Self::load_from_yaml(path)?;
185 conf.merge_with_opt(opt);
186 Ok(conf)
187 } else {
188 Error::e_explain(ReadError, "No path specified")
189 }
190 }
191
192 pub fn new() -> Option<Self> {
193 Self::from_yaml("---\nversion: 1").ok()
194 }
195
196 pub fn new_with_opt_override(opt: &Opt) -> Option<Self> {
197 let conf = Self::new();
198 match conf {
199 Some(mut c) => {
200 c.merge_with_opt(opt);
201 Some(c)
202 }
203 None => None,
204 }
205 }
206
207 pub fn from_yaml(conf_str: &str) -> Result<Self> {
208 trace!("Read conf file: {conf_str}");
209 let conf: ServerConf = serde_yaml::from_str(conf_str).or_err_with(ReadError, || {
210 format!("Unable to parse yaml conf {conf_str}")
211 })?;
212
213 trace!("Loaded conf: {conf:?}");
214 conf.validate()
215 }
216
217 pub fn to_yaml(&self) -> String {
218 serde_yaml::to_string(self).unwrap()
219 }
220
221 pub fn validate(self) -> Result<Self> {
222 // TODO: do the validation
223 Ok(self)
224 }
225
226 pub fn merge_with_opt(&mut self, opt: &Opt) {
227 if opt.daemon {
228 self.daemon = true;
229 }
230 }
231}
232
233/// Create an instance of Opt by parsing the current command-line args.
234/// This is equivalent to running `Opt::parse` but does not require the
235/// caller to have included the `clap::Parser`
236impl Opt {
237 pub fn parse_args() -> Self {
238 Opt::parse()
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn init_log() {
247 let _ = env_logger::builder().is_test(true).try_init();
248 }
249
250 #[test]
251 fn not_a_test_i_cannot_write_yaml_by_hand() {
252 init_log();
253 let conf = ServerConf {
254 version: 1,
255 client_bind_to_ipv4: vec!["1.2.3.4".to_string(), "5.6.7.8".to_string()],
256 client_bind_to_ipv6: vec![],
257 ca_file: None,
258 daemon: false,
259 error_log: None,
260 upstream_debug_ssl_keylog: false,
261 pid_file: "".to_string(),
262 upgrade_sock: "".to_string(),
263 user: None,
264 group: None,
265 threads: 1,
266 work_stealing: true,
267 upstream_keepalive_pool_size: 4,
268 upstream_connect_offload_threadpools: None,
269 upstream_connect_offload_thread_per_pool: None,
270 grace_period_seconds: None,
271 graceful_shutdown_timeout_seconds: None,
272 };
273 // cargo test -- --nocapture not_a_test_i_cannot_write_yaml_by_hand
274 println!("{}", conf.to_yaml());
275 }
276
277 #[test]
278 fn test_load_file() {
279 init_log();
280 let conf_str = r#"
281---
282version: 1
283client_bind_to_ipv4:
284 - 1.2.3.4
285 - 5.6.7.8
286client_bind_to_ipv6: []
287 "#
288 .to_string();
289 let conf = ServerConf::from_yaml(&conf_str).unwrap();
290 assert_eq!(2, conf.client_bind_to_ipv4.len());
291 assert_eq!(0, conf.client_bind_to_ipv6.len());
292 assert_eq!(1, conf.version);
293 }
294
295 #[test]
296 fn test_default() {
297 init_log();
298 let conf_str = r#"
299---
300version: 1
301 "#
302 .to_string();
303 let conf = ServerConf::from_yaml(&conf_str).unwrap();
304 assert_eq!(0, conf.client_bind_to_ipv4.len());
305 assert_eq!(0, conf.client_bind_to_ipv6.len());
306 assert_eq!(1, conf.version);
307 assert_eq!("/tmp/pingora.pid", conf.pid_file);
308 }
309}