conjure_runtime_config/
lib.rs

1// Copyright 2020 Palantir Technologies, 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//! Deserializable configuration types for `conjure_runtime` clients.
15#![warn(missing_docs, clippy::all)]
16// reserve the right to add non-eq config in the future
17#![allow(clippy::derive_partial_eq_without_eq)]
18
19use serde::de::{Deserializer, Error as _, Unexpected};
20use serde::Deserialize;
21use staged_builder::staged_builder;
22use std::collections::HashMap;
23use std::fmt;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26use url::Url;
27
28#[cfg(test)]
29mod test;
30
31/// Configuration for a collection of services.
32///
33/// This type can be constructed programmatically via the `ServicesConfigBuilder` API or deserialized from e.g. a
34/// configuration file. Default values for various configuration options can be set at the top level in addition to
35/// being specified per-service.
36///
37/// # Examples
38///
39/// ```yaml
40/// services:
41///   auth-service:
42///     uris:
43///       - https://auth.my-network.com:1234/auth-service
44///   cache-service:
45///     uris:
46///       - https://cache-1.my-network.com/cache-service
47///       - https://cache-2.my-network.com/cache-service
48///     request-timeout: 10s
49/// # options set at this level will apply as defaults to all configured services
50/// security:
51///   ca-file: var/security/ca.pem
52/// ```
53#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
54#[serde(rename_all = "kebab-case", default)]
55#[staged_builder]
56#[builder(update)]
57pub struct ServicesConfig {
58    #[builder(map(key(type = String, into), value(type = ServiceConfig)))]
59    services: HashMap<String, ServiceConfig>,
60    #[builder(default, into)]
61    security: Option<SecurityConfig>,
62    #[builder(default, into)]
63    proxy: Option<ProxyConfig>,
64    #[builder(default, into)]
65    #[serde(deserialize_with = "de_opt_duration")]
66    connect_timeout: Option<Duration>,
67    #[builder(default, into)]
68    #[serde(deserialize_with = "de_opt_duration")]
69    read_timeout: Option<Duration>,
70    #[builder(default, into)]
71    #[serde(deserialize_with = "de_opt_duration")]
72    write_timeout: Option<Duration>,
73    #[builder(default, into)]
74    #[serde(deserialize_with = "de_opt_duration")]
75    backoff_slot_size: Option<Duration>,
76}
77
78impl ServicesConfig {
79    /// Returns the configuration for the specified service with top-level defaults applied.
80    pub fn merged_service(&self, name: &str) -> Option<ServiceConfig> {
81        let mut service = self.services.get(name).cloned()?;
82
83        if service.security.is_none() {
84            service.security = self.security.clone();
85        }
86
87        if service.proxy.is_none() {
88            service.proxy = self.proxy.clone();
89        }
90
91        if service.connect_timeout.is_none() {
92            service.connect_timeout = self.connect_timeout;
93        }
94
95        if service.read_timeout.is_none() {
96            service.read_timeout = self.read_timeout;
97        }
98
99        if service.write_timeout.is_none() {
100            service.write_timeout = self.write_timeout;
101        }
102
103        if service.backoff_slot_size.is_none() {
104            service.backoff_slot_size = self.backoff_slot_size;
105        }
106
107        Some(service)
108    }
109
110    /// Returns the security configuration.
111    pub fn security(&self) -> Option<&SecurityConfig> {
112        self.security.as_ref()
113    }
114
115    /// Returns the proxy configuration.
116    pub fn proxy(&self) -> Option<&ProxyConfig> {
117        self.proxy.as_ref()
118    }
119
120    /// Returns the connection timeout.
121    pub fn connect_timeout(&self) -> Option<Duration> {
122        self.connect_timeout
123    }
124
125    /// Returns the read timeout.
126    pub fn read_timeout(&self) -> Option<Duration> {
127        self.read_timeout
128    }
129
130    /// Returns the write timeout.
131    pub fn write_timeout(&self) -> Option<Duration> {
132        self.write_timeout
133    }
134
135    /// Returns the backoff slot size.
136    pub fn backoff_slot_size(&self) -> Option<Duration> {
137        self.backoff_slot_size
138    }
139}
140
141/// The configuration for an individual service.
142#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
143#[serde(rename_all = "kebab-case", default)]
144#[staged_builder]
145#[builder(update)]
146pub struct ServiceConfig {
147    #[builder(list(item(type = Url)))]
148    uris: Vec<Url>,
149    #[builder(default, into)]
150    security: Option<SecurityConfig>,
151    #[builder(default, into)]
152    proxy: Option<ProxyConfig>,
153    #[builder(default, into)]
154    #[serde(deserialize_with = "de_opt_duration")]
155    connect_timeout: Option<Duration>,
156    #[builder(default, into)]
157    #[serde(deserialize_with = "de_opt_duration")]
158    read_timeout: Option<Duration>,
159    #[builder(default, into)]
160    #[serde(deserialize_with = "de_opt_duration")]
161    write_timeout: Option<Duration>,
162    #[builder(default, into)]
163    #[serde(deserialize_with = "de_opt_duration")]
164    backoff_slot_size: Option<Duration>,
165    #[builder(default, into)]
166    max_num_retries: Option<u32>,
167}
168
169impl ServiceConfig {
170    /// Returns the URIs of the service's nodes.
171    pub fn uris(&self) -> &[Url] {
172        &self.uris
173    }
174
175    /// Returns the security configuration.
176    pub fn security(&self) -> Option<&SecurityConfig> {
177        self.security.as_ref()
178    }
179
180    /// Returns the proxy configuration.
181    pub fn proxy(&self) -> Option<&ProxyConfig> {
182        self.proxy.as_ref()
183    }
184
185    /// Returns the connection timeout.
186    pub fn connect_timeout(&self) -> Option<Duration> {
187        self.connect_timeout
188    }
189
190    /// Returns the read timeout.
191    pub fn read_timeout(&self) -> Option<Duration> {
192        self.read_timeout
193    }
194
195    /// Returns the write timeout.
196    pub fn write_timeout(&self) -> Option<Duration> {
197        self.write_timeout
198    }
199
200    /// Returns the backoff slot size.
201    pub fn backoff_slot_size(&self) -> Option<Duration> {
202        self.backoff_slot_size
203    }
204
205    /// Returns the maximum number of retries for failed RPCs.
206    pub fn max_num_retries(&self) -> Option<u32> {
207        self.max_num_retries
208    }
209}
210
211/// Security configuration used to communicate with a service.
212#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Deserialize)]
213#[serde(rename_all = "kebab-case")]
214#[staged_builder]
215#[builder(update)]
216pub struct SecurityConfig {
217    #[builder(default, into)]
218    ca_file: Option<PathBuf>,
219    #[builder(default, into)]
220    key_file: Option<PathBuf>,
221    #[builder(default, into)]
222    cert_file: Option<PathBuf>,
223}
224
225impl SecurityConfig {
226    /// The path to a file containing PEM-formatted root certificates trusted to identify the service.
227    ///
228    /// These certificates are used in addition to the bundled root CA list.
229    pub fn ca_file(&self) -> Option<&Path> {
230        self.ca_file.as_deref()
231    }
232
233    /// The path to a file containing a PEM-formatted private key used for client certificate authentication.
234    ///
235    /// This key is expected to match the leaf certificate in [`Self::cert_file`].
236    pub fn key_file(&self) -> Option<&Path> {
237        self.key_file.as_deref()
238    }
239
240    /// The path to a file containing PEM-formatted certificates used for client certificate authentication.
241    ///
242    /// The file should start with the leaf certificate corresponding to the key in [`Self::key_file`], and the contain
243    /// the remainder of the certificate chain to a trusted root.
244    pub fn cert_file(&self) -> Option<&Path> {
245        self.cert_file.as_deref()
246    }
247}
248
249/// Proxy configuration used to connect to a service.
250#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
251#[serde(rename_all = "kebab-case", tag = "type")]
252#[non_exhaustive]
253pub enum ProxyConfig {
254    /// A direct connection (i.e. no proxy).
255    Direct,
256    /// An HTTP proxy.
257    Http(HttpProxyConfig),
258}
259
260#[allow(clippy::derivable_impls)]
261impl Default for ProxyConfig {
262    fn default() -> ProxyConfig {
263        ProxyConfig::Direct
264    }
265}
266
267/// Configuration for an HTTP proxy.
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
269#[serde(rename_all = "kebab-case")]
270#[staged_builder]
271#[builder(update)]
272pub struct HttpProxyConfig {
273    host_and_port: HostAndPort,
274    #[builder(default, into)]
275    credentials: Option<BasicCredentials>,
276}
277
278impl HttpProxyConfig {
279    /// The host and port of the proxy server.
280    pub fn host_and_port(&self) -> &HostAndPort {
281        &self.host_and_port
282    }
283
284    /// The credentials used to authenticate with the proxy.
285    pub fn credentials(&self) -> Option<&BasicCredentials> {
286        self.credentials.as_ref()
287    }
288}
289
290/// A host and port identifier of a server.
291#[derive(Debug, Clone, PartialEq, Eq, Hash)]
292pub struct HostAndPort {
293    host: String,
294    port: u16,
295}
296
297impl fmt::Display for HostAndPort {
298    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
299        write!(fmt, "{}:{}", self.host, self.port)
300    }
301}
302
303impl<'de> Deserialize<'de> for HostAndPort {
304    fn deserialize<D>(deserializer: D) -> Result<HostAndPort, D::Error>
305    where
306        D: Deserializer<'de>,
307    {
308        let expected = "a host:port identifier";
309
310        let mut s = String::deserialize(deserializer)?;
311
312        match s.find(':') {
313            Some(idx) => {
314                let port = s[idx + 1..]
315                    .parse()
316                    .map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &expected))?;
317                s.truncate(idx);
318                Ok(HostAndPort { host: s, port })
319            }
320            None => Err(D::Error::invalid_value(Unexpected::Str(&s), &expected)),
321        }
322    }
323}
324
325impl HostAndPort {
326    /// Creates a new `HostAndPort`.
327    pub fn new(host: &str, port: u16) -> HostAndPort {
328        HostAndPort {
329            host: host.to_string(),
330            port,
331        }
332    }
333
334    /// Returns the host.
335    pub fn host(&self) -> &str {
336        &self.host
337    }
338
339    /// Returns the port.
340    pub fn port(&self) -> u16 {
341        self.port
342    }
343}
344
345/// Credentials used to authenticate with an HTTP proxy.
346#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
347pub struct BasicCredentials {
348    username: String,
349    password: String,
350}
351
352impl BasicCredentials {
353    /// Creates a new `BasicCredentials.
354    pub fn new(username: &str, password: &str) -> BasicCredentials {
355        BasicCredentials {
356            username: username.to_string(),
357            password: password.to_string(),
358        }
359    }
360
361    /// Returns the username.
362    pub fn username(&self) -> &str {
363        &self.username
364    }
365
366    /// Returns the password.
367    pub fn password(&self) -> &str {
368        &self.password
369    }
370}
371
372fn de_opt_duration<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
373where
374    D: Deserializer<'de>,
375{
376    humantime_serde::Serde::deserialize(d).map(humantime_serde::Serde::into_inner)
377}