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}