Skip to main content

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    #[serde(default)]
224    #[builder(list(item(type = String, into)))]
225    pinned_certs: Vec<String>,
226}
227
228impl SecurityConfig {
229    /// The path to a file containing PEM-formatted root certificates trusted to identify the service.
230    ///
231    /// These certificates are used in addition to the bundled root CA list.
232    pub fn ca_file(&self) -> Option<&Path> {
233        self.ca_file.as_deref()
234    }
235
236    /// The path to a file containing a PEM-formatted private key used for client certificate authentication.
237    ///
238    /// This key is expected to match the leaf certificate in [`Self::cert_file`].
239    pub fn key_file(&self) -> Option<&Path> {
240        self.key_file.as_deref()
241    }
242
243    /// The path to a file containing PEM-formatted certificates used for client certificate authentication.
244    ///
245    /// The file should start with the leaf certificate corresponding to the key in [`Self::key_file`], and the contain
246    /// the remainder of the certificate chain to a trusted root.
247    pub fn cert_file(&self) -> Option<&Path> {
248        self.cert_file.as_deref()
249    }
250
251    /// PEM-formatted leaf certificates to pin against, one PEM document per entry.
252    ///
253    /// When non-empty, the client will only consider a TLS connection valid if the server's end-entity certificate
254    /// matches one of the certificates in this list. The certificate chain itself is *not* validated against any
255    /// trust anchor; pinning the leaf is sufficient to identify the server. This is intended as an escape hatch for
256    /// environments whose CA hierarchy uses X.509 features (e.g. `directoryName` name constraints, see
257    /// [RFC 5280 ยง4.2.1.10](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10)) that the underlying
258    /// TLS library does not support.
259    ///
260    /// Because the chain is skipped, the configured certificates must be rotated in lockstep with the server's leaf
261    /// certificate. Operators that enable this option should ensure they have a process to keep the pins up to date,
262    /// or service traffic will fail when the server's certificate is renewed.
263    pub fn pinned_certs(&self) -> &[String] {
264        &self.pinned_certs
265    }
266}
267
268/// Proxy configuration used to connect to a service.
269#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
270#[serde(rename_all = "kebab-case", tag = "type")]
271#[non_exhaustive]
272pub enum ProxyConfig {
273    /// A direct connection (i.e. no proxy).
274    Direct,
275    /// An HTTP proxy.
276    Http(HttpProxyConfig),
277}
278
279#[allow(clippy::derivable_impls)]
280impl Default for ProxyConfig {
281    fn default() -> ProxyConfig {
282        ProxyConfig::Direct
283    }
284}
285
286/// Configuration for an HTTP proxy.
287#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
288#[serde(rename_all = "kebab-case")]
289#[staged_builder]
290#[builder(update)]
291pub struct HttpProxyConfig {
292    host_and_port: HostAndPort,
293    #[builder(default, into)]
294    credentials: Option<BasicCredentials>,
295}
296
297impl HttpProxyConfig {
298    /// The host and port of the proxy server.
299    pub fn host_and_port(&self) -> &HostAndPort {
300        &self.host_and_port
301    }
302
303    /// The credentials used to authenticate with the proxy.
304    pub fn credentials(&self) -> Option<&BasicCredentials> {
305        self.credentials.as_ref()
306    }
307}
308
309/// A host and port identifier of a server.
310#[derive(Debug, Clone, PartialEq, Eq, Hash)]
311pub struct HostAndPort {
312    host: String,
313    port: u16,
314}
315
316impl fmt::Display for HostAndPort {
317    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
318        write!(fmt, "{}:{}", self.host, self.port)
319    }
320}
321
322impl<'de> Deserialize<'de> for HostAndPort {
323    fn deserialize<D>(deserializer: D) -> Result<HostAndPort, D::Error>
324    where
325        D: Deserializer<'de>,
326    {
327        let expected = "a host:port identifier";
328
329        let mut s = String::deserialize(deserializer)?;
330
331        match s.find(':') {
332            Some(idx) => {
333                let port = s[idx + 1..]
334                    .parse()
335                    .map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &expected))?;
336                s.truncate(idx);
337                Ok(HostAndPort { host: s, port })
338            }
339            None => Err(D::Error::invalid_value(Unexpected::Str(&s), &expected)),
340        }
341    }
342}
343
344impl HostAndPort {
345    /// Creates a new `HostAndPort`.
346    pub fn new(host: &str, port: u16) -> HostAndPort {
347        HostAndPort {
348            host: host.to_string(),
349            port,
350        }
351    }
352
353    /// Returns the host.
354    pub fn host(&self) -> &str {
355        &self.host
356    }
357
358    /// Returns the port.
359    pub fn port(&self) -> u16 {
360        self.port
361    }
362}
363
364/// Credentials used to authenticate with an HTTP proxy.
365#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
366pub struct BasicCredentials {
367    username: String,
368    password: String,
369}
370
371impl BasicCredentials {
372    /// Creates a new `BasicCredentials.
373    pub fn new(username: &str, password: &str) -> BasicCredentials {
374        BasicCredentials {
375            username: username.to_string(),
376            password: password.to_string(),
377        }
378    }
379
380    /// Returns the username.
381    pub fn username(&self) -> &str {
382        &self.username
383    }
384
385    /// Returns the password.
386    pub fn password(&self) -> &str {
387        &self.password
388    }
389}
390
391fn de_opt_duration<'de, D>(d: D) -> Result<Option<Duration>, D::Error>
392where
393    D: Deserializer<'de>,
394{
395    humantime_serde::Serde::deserialize(d).map(humantime_serde::Serde::into_inner)
396}