1use std::borrow::Cow;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use anyhow::Context;
8use anyhow::Result;
9use anyhow::anyhow;
10use anyhow::bail;
11use crankshaft::events::Event;
12use indexmap::IndexMap;
13use secrecy::ExposeSecret;
14use serde::Deserialize;
15use serde::Serialize;
16use tokio::sync::broadcast;
17use tracing::warn;
18use url::Url;
19
20use crate::DockerBackend;
21use crate::LocalBackend;
22use crate::SYSTEM;
23use crate::TaskExecutionBackend;
24use crate::TesBackend;
25use crate::convert_unit_string;
26use crate::path::is_url;
27
28pub const MAX_RETRIES: u64 = 100;
30
31pub const DEFAULT_TASK_SHELL: &str = "bash";
33
34pub const DEFAULT_BACKEND_NAME: &str = "default";
36
37const REDACTED: &str = "<REDACTED>";
39
40#[derive(Debug, Clone)]
44pub struct SecretString {
45 inner: secrecy::SecretString,
49 redacted: bool,
56}
57
58impl SecretString {
59 pub fn redact(&mut self) {
64 self.redacted = true;
65 }
66
67 pub fn unredact(&mut self) {
69 self.redacted = false;
70 }
71
72 pub fn inner(&self) -> &secrecy::SecretString {
74 &self.inner
75 }
76}
77
78impl From<String> for SecretString {
79 fn from(s: String) -> Self {
80 Self {
81 inner: s.into(),
82 redacted: true,
83 }
84 }
85}
86
87impl From<&str> for SecretString {
88 fn from(s: &str) -> Self {
89 Self {
90 inner: s.into(),
91 redacted: true,
92 }
93 }
94}
95
96impl Default for SecretString {
97 fn default() -> Self {
98 Self {
99 inner: Default::default(),
100 redacted: true,
101 }
102 }
103}
104
105impl serde::Serialize for SecretString {
106 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
107 where
108 S: serde::Serializer,
109 {
110 use secrecy::ExposeSecret;
111
112 if self.redacted {
113 serializer.serialize_str(REDACTED)
114 } else {
115 serializer.serialize_str(self.inner.expose_secret())
116 }
117 }
118}
119
120impl<'de> serde::Deserialize<'de> for SecretString {
121 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
122 where
123 D: serde::Deserializer<'de>,
124 {
125 let inner = secrecy::SecretString::deserialize(deserializer)?;
126 Ok(Self {
127 inner,
128 redacted: true,
129 })
130 }
131}
132
133#[derive(Debug, Default, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case", deny_unknown_fields)]
145pub struct Config {
146 #[serde(default)]
148 pub http: HttpConfig,
149 #[serde(default)]
151 pub workflow: WorkflowConfig,
152 #[serde(default)]
154 pub task: TaskConfig,
155 #[serde(skip_serializing_if = "Option::is_none")]
160 pub backend: Option<String>,
161 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
169 pub backends: IndexMap<String, BackendConfig>,
170 #[serde(default)]
172 pub storage: StorageConfig,
173 #[serde(default)]
187 pub suppress_env_specific_output: bool,
188}
189
190impl Config {
191 pub fn validate(&self) -> Result<()> {
193 self.http.validate()?;
194 self.workflow.validate()?;
195 self.task.validate()?;
196
197 if self.backend.is_none() && self.backends.len() < 2 {
198 } else {
201 let backend = self.backend.as_deref().unwrap_or(DEFAULT_BACKEND_NAME);
203 if !self.backends.contains_key(backend) {
204 bail!("a backend named `{backend}` is not present in the configuration");
205 }
206 }
207
208 for backend in self.backends.values() {
209 backend.validate()?;
210 }
211
212 self.storage.validate()?;
213 Ok(())
214 }
215
216 pub fn redact(&mut self) {
220 for backend in self.backends.values_mut() {
221 backend.redact();
222 }
223
224 self.storage.azure.redact();
225
226 if let Some(auth) = &mut self.storage.s3.auth {
227 auth.redact();
228 }
229
230 if let Some(auth) = &mut self.storage.google.auth {
231 auth.redact();
232 }
233 }
234
235 pub fn unredact(&mut self) {
239 for backend in self.backends.values_mut() {
240 backend.unredact();
241 }
242
243 self.storage.azure.unredact();
244
245 if let Some(auth) = &mut self.storage.s3.auth {
246 auth.unredact();
247 }
248
249 if let Some(auth) = &mut self.storage.google.auth {
250 auth.unredact();
251 }
252 }
253
254 pub async fn create_backend(
256 self: &Arc<Self>,
257 events: Option<broadcast::Sender<Event>>,
258 ) -> Result<Arc<dyn TaskExecutionBackend>> {
259 let config = if self.backend.is_none() && self.backends.len() < 2 {
260 if self.backends.len() == 1 {
261 Cow::Borrowed(self.backends.values().next().unwrap())
263 } else {
264 Cow::Owned(BackendConfig::default())
266 }
267 } else {
268 let backend = self.backend.as_deref().unwrap_or(DEFAULT_BACKEND_NAME);
270 Cow::Borrowed(self.backends.get(backend).ok_or_else(|| {
271 anyhow!("a backend named `{backend}` is not present in the configuration")
272 })?)
273 };
274
275 match config.as_ref() {
276 BackendConfig::Local(config) => {
277 warn!(
278 "the engine is configured to use the local backend: tasks will not be run \
279 inside of a container"
280 );
281 Ok(Arc::new(LocalBackend::new(self.clone(), config, events)?))
282 }
283 BackendConfig::Docker(config) => Ok(Arc::new(
284 DockerBackend::new(self.clone(), config, events).await?,
285 )),
286 BackendConfig::Tes(config) => Ok(Arc::new(
287 TesBackend::new(self.clone(), config, events).await?,
288 )),
289 }
290 }
291}
292
293#[derive(Debug, Default, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "snake_case", deny_unknown_fields)]
296pub struct HttpConfig {
297 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub cache: Option<PathBuf>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub retries: Option<usize>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub parallelism: Option<usize>,
312}
313
314impl HttpConfig {
315 pub fn validate(&self) -> Result<()> {
317 if let Some(parallelism) = self.parallelism
318 && parallelism == 0
319 {
320 bail!("configuration value `http.parallelism` cannot be zero");
321 }
322 Ok(())
323 }
324}
325
326#[derive(Debug, Default, Clone, Serialize, Deserialize)]
328#[serde(rename_all = "snake_case", deny_unknown_fields)]
329pub struct StorageConfig {
330 #[serde(default)]
332 pub azure: AzureStorageConfig,
333 #[serde(default)]
335 pub s3: S3StorageConfig,
336 #[serde(default)]
338 pub google: GoogleStorageConfig,
339}
340
341impl StorageConfig {
342 pub fn validate(&self) -> Result<()> {
344 self.azure.validate()?;
345 self.s3.validate()?;
346 self.google.validate()?;
347 Ok(())
348 }
349}
350
351#[derive(Debug, Default, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case", deny_unknown_fields)]
354pub struct AzureStorageConfig {
355 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
364 pub auth: IndexMap<String, IndexMap<String, SecretString>>,
365}
366
367impl AzureStorageConfig {
368 pub fn validate(&self) -> Result<()> {
370 Ok(())
371 }
372
373 pub fn redact(&mut self) {
375 for v in self.auth.values_mut() {
376 for v in v.values_mut() {
377 v.redact();
378 }
379 }
380 }
381
382 pub fn unredact(&mut self) {
384 for v in self.auth.values_mut() {
385 for v in v.values_mut() {
386 v.unredact();
387 }
388 }
389 }
390}
391
392#[derive(Debug, Default, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "snake_case", deny_unknown_fields)]
395pub struct S3StorageAuthConfig {
396 pub access_key_id: String,
398 pub secret_access_key: SecretString,
400}
401
402impl S3StorageAuthConfig {
403 pub fn validate(&self) -> Result<()> {
405 if self.access_key_id.is_empty() {
406 bail!("configuration value `storage.s3.auth.access_key_id` is required");
407 }
408
409 if self.secret_access_key.inner.expose_secret().is_empty() {
410 bail!("configuration value `storage.s3.auth.secret_access_key` is required");
411 }
412
413 Ok(())
414 }
415
416 pub fn redact(&mut self) {
419 self.secret_access_key.redact();
420 }
421
422 pub fn unredact(&mut self) {
425 self.secret_access_key.unredact();
426 }
427}
428
429#[derive(Debug, Default, Clone, Serialize, Deserialize)]
431#[serde(rename_all = "snake_case", deny_unknown_fields)]
432pub struct S3StorageConfig {
433 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub region: Option<String>,
439
440 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub auth: Option<S3StorageAuthConfig>,
443}
444
445impl S3StorageConfig {
446 pub fn validate(&self) -> Result<()> {
448 if let Some(auth) = &self.auth {
449 auth.validate()?;
450 }
451
452 Ok(())
453 }
454}
455
456#[derive(Debug, Default, Clone, Serialize, Deserialize)]
458#[serde(rename_all = "snake_case", deny_unknown_fields)]
459pub struct GoogleStorageAuthConfig {
460 pub access_key: String,
462 pub secret: SecretString,
464}
465
466impl GoogleStorageAuthConfig {
467 pub fn validate(&self) -> Result<()> {
469 if self.access_key.is_empty() {
470 bail!("configuration value `storage.google.auth.access_key` is required");
471 }
472
473 if self.secret.inner.expose_secret().is_empty() {
474 bail!("configuration value `storage.google.auth.secret` is required");
475 }
476
477 Ok(())
478 }
479
480 pub fn redact(&mut self) {
483 self.secret.redact();
484 }
485
486 pub fn unredact(&mut self) {
489 self.secret.unredact();
490 }
491}
492
493#[derive(Debug, Default, Clone, Serialize, Deserialize)]
495#[serde(rename_all = "snake_case", deny_unknown_fields)]
496pub struct GoogleStorageConfig {
497 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub auth: Option<GoogleStorageAuthConfig>,
500}
501
502impl GoogleStorageConfig {
503 pub fn validate(&self) -> Result<()> {
505 if let Some(auth) = &self.auth {
506 auth.validate()?;
507 }
508
509 Ok(())
510 }
511}
512
513#[derive(Debug, Default, Clone, Serialize, Deserialize)]
515#[serde(rename_all = "snake_case", deny_unknown_fields)]
516pub struct WorkflowConfig {
517 #[serde(default)]
519 pub scatter: ScatterConfig,
520}
521
522impl WorkflowConfig {
523 pub fn validate(&self) -> Result<()> {
525 self.scatter.validate()?;
526 Ok(())
527 }
528}
529
530#[derive(Debug, Default, Clone, Serialize, Deserialize)]
532#[serde(rename_all = "snake_case", deny_unknown_fields)]
533pub struct ScatterConfig {
534 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub concurrency: Option<u64>,
588}
589
590impl ScatterConfig {
591 pub fn validate(&self) -> Result<()> {
593 if let Some(concurrency) = self.concurrency
594 && concurrency == 0
595 {
596 bail!("configuration value `workflow.scatter.concurrency` cannot be zero");
597 }
598
599 Ok(())
600 }
601}
602
603#[derive(Debug, Default, Clone, Serialize, Deserialize)]
605#[serde(rename_all = "snake_case", deny_unknown_fields)]
606pub struct TaskConfig {
607 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub retries: Option<u64>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub container: Option<String>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub shell: Option<String>,
629 #[serde(default)]
631 pub cpu_limit_behavior: TaskResourceLimitBehavior,
632 #[serde(default)]
634 pub memory_limit_behavior: TaskResourceLimitBehavior,
635}
636
637impl TaskConfig {
638 pub fn validate(&self) -> Result<()> {
640 if self.retries.unwrap_or(0) > MAX_RETRIES {
641 bail!("configuration value `task.retries` cannot exceed {MAX_RETRIES}");
642 }
643
644 Ok(())
645 }
646}
647
648#[derive(Debug, Default, Clone, Serialize, Deserialize)]
651#[serde(rename_all = "snake_case", deny_unknown_fields)]
652pub enum TaskResourceLimitBehavior {
653 TryWithMax,
656 #[default]
660 Deny,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize)]
665#[serde(rename_all = "snake_case", tag = "type")]
666pub enum BackendConfig {
667 Local(LocalBackendConfig),
669 Docker(DockerBackendConfig),
671 Tes(Box<TesBackendConfig>),
673}
674
675impl Default for BackendConfig {
676 fn default() -> Self {
677 Self::Docker(Default::default())
678 }
679}
680
681impl BackendConfig {
682 pub fn validate(&self) -> Result<()> {
684 match self {
685 Self::Local(config) => config.validate(),
686 Self::Docker(config) => config.validate(),
687 Self::Tes(config) => config.validate(),
688 }
689 }
690
691 pub fn as_local(&self) -> Option<&LocalBackendConfig> {
695 match self {
696 Self::Local(config) => Some(config),
697 _ => None,
698 }
699 }
700
701 pub fn as_docker(&self) -> Option<&DockerBackendConfig> {
705 match self {
706 Self::Docker(config) => Some(config),
707 _ => None,
708 }
709 }
710
711 pub fn as_tes(&self) -> Option<&TesBackendConfig> {
715 match self {
716 Self::Tes(config) => Some(config),
717 _ => None,
718 }
719 }
720
721 pub fn redact(&mut self) {
723 match self {
724 Self::Local(_) | Self::Docker(_) => {}
725 Self::Tes(config) => config.redact(),
726 }
727 }
728
729 pub fn unredact(&mut self) {
731 match self {
732 Self::Local(_) | Self::Docker(_) => {}
733 Self::Tes(config) => config.unredact(),
734 }
735 }
736}
737
738#[derive(Debug, Default, Clone, Serialize, Deserialize)]
745#[serde(rename_all = "snake_case", deny_unknown_fields)]
746pub struct LocalBackendConfig {
747 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub cpu: Option<u64>,
754
755 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub memory: Option<String>,
763}
764
765impl LocalBackendConfig {
766 pub fn validate(&self) -> Result<()> {
768 if let Some(cpu) = self.cpu {
769 if cpu == 0 {
770 bail!("local backend configuration value `cpu` cannot be zero");
771 }
772
773 let total = SYSTEM.cpus().len() as u64;
774 if cpu > total {
775 bail!(
776 "local backend configuration value `cpu` cannot exceed the virtual CPUs \
777 available to the host ({total})"
778 );
779 }
780 }
781
782 if let Some(memory) = &self.memory {
783 let memory = convert_unit_string(memory).with_context(|| {
784 format!("local backend configuration value `memory` has invalid value `{memory}`")
785 })?;
786
787 if memory == 0 {
788 bail!("local backend configuration value `memory` cannot be zero");
789 }
790
791 let total = SYSTEM.total_memory();
792 if memory > total {
793 bail!(
794 "local backend configuration value `memory` cannot exceed the total memory of \
795 the host ({total} bytes)"
796 );
797 }
798 }
799
800 Ok(())
801 }
802}
803
804const fn cleanup_default() -> bool {
806 true
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize)]
811#[serde(rename_all = "snake_case", deny_unknown_fields)]
812pub struct DockerBackendConfig {
813 #[serde(default = "cleanup_default")]
817 pub cleanup: bool,
818}
819
820impl DockerBackendConfig {
821 pub fn validate(&self) -> Result<()> {
823 Ok(())
824 }
825}
826
827impl Default for DockerBackendConfig {
828 fn default() -> Self {
829 Self { cleanup: true }
830 }
831}
832
833#[derive(Debug, Default, Clone, Serialize, Deserialize)]
835#[serde(rename_all = "snake_case", deny_unknown_fields)]
836pub struct BasicAuthConfig {
837 #[serde(default)]
839 pub username: String,
840 #[serde(default)]
842 pub password: SecretString,
843}
844
845impl BasicAuthConfig {
846 pub fn validate(&self) -> Result<()> {
848 Ok(())
849 }
850
851 pub fn redact(&mut self) {
853 self.password.redact();
854 }
855
856 pub fn unredact(&mut self) {
858 self.password.unredact();
859 }
860}
861
862#[derive(Debug, Default, Clone, Serialize, Deserialize)]
864#[serde(rename_all = "snake_case", deny_unknown_fields)]
865pub struct BearerAuthConfig {
866 #[serde(default)]
868 pub token: SecretString,
869}
870
871impl BearerAuthConfig {
872 pub fn validate(&self) -> Result<()> {
874 Ok(())
875 }
876
877 pub fn redact(&mut self) {
879 self.token.redact();
880 }
881
882 pub fn unredact(&mut self) {
884 self.token.unredact();
885 }
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize)]
890#[serde(rename_all = "snake_case", tag = "type")]
891pub enum TesBackendAuthConfig {
892 Basic(BasicAuthConfig),
894 Bearer(BearerAuthConfig),
896}
897
898impl TesBackendAuthConfig {
899 pub fn validate(&self) -> Result<()> {
901 match self {
902 Self::Basic(config) => config.validate(),
903 Self::Bearer(config) => config.validate(),
904 }
905 }
906
907 pub fn redact(&mut self) {
910 match self {
911 Self::Basic(auth) => auth.redact(),
912 Self::Bearer(auth) => auth.redact(),
913 }
914 }
915
916 pub fn unredact(&mut self) {
919 match self {
920 Self::Basic(auth) => auth.unredact(),
921 Self::Bearer(auth) => auth.unredact(),
922 }
923 }
924}
925
926#[derive(Debug, Default, Clone, Serialize, Deserialize)]
928#[serde(rename_all = "snake_case", deny_unknown_fields)]
929pub struct TesBackendConfig {
930 #[serde(default, skip_serializing_if = "Option::is_none")]
932 pub url: Option<Url>,
933
934 #[serde(default, skip_serializing_if = "Option::is_none")]
936 pub auth: Option<TesBackendAuthConfig>,
937
938 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub inputs: Option<Url>,
941
942 #[serde(default, skip_serializing_if = "Option::is_none")]
944 pub outputs: Option<Url>,
945
946 #[serde(default, skip_serializing_if = "Option::is_none")]
950 pub interval: Option<u64>,
951
952 #[serde(default, skip_serializing_if = "Option::is_none")]
956 pub max_concurrency: Option<u64>,
957
958 #[serde(default)]
961 pub insecure: bool,
962}
963
964impl TesBackendConfig {
965 pub fn validate(&self) -> Result<()> {
967 match &self.url {
968 Some(url) => {
969 if !self.insecure && url.scheme() != "https" {
970 bail!(
971 "TES backend configuration value `url` has invalid value `{url}`: URL \
972 must use a HTTPS scheme"
973 );
974 }
975 }
976 None => bail!("TES backend configuration value `url` is required"),
977 }
978
979 if let Some(auth) = &self.auth {
980 auth.validate()?;
981 }
982
983 match &self.inputs {
984 Some(url) => {
985 if !is_url(url.as_str()) {
986 bail!(
987 "TES backend storage configuration value `inputs` has invalid value \
988 `{url}`: URL scheme is not supported"
989 );
990 }
991
992 if !url.path().ends_with('/') {
993 bail!(
994 "TES backend storage configuration value `inputs` has invalid value \
995 `{url}`: URL path must end with a slash"
996 );
997 }
998 }
999 None => bail!("TES backend configuration value `inputs` is required"),
1000 }
1001
1002 match &self.outputs {
1003 Some(url) => {
1004 if !is_url(url.as_str()) {
1005 bail!(
1006 "TES backend storage configuration value `outputs` has invalid value \
1007 `{url}`: URL scheme is not supported"
1008 );
1009 }
1010
1011 if !url.path().ends_with('/') {
1012 bail!(
1013 "TES backend storage configuration value `outputs` has invalid value \
1014 `{url}`: URL path must end with a slash"
1015 );
1016 }
1017 }
1018 None => bail!("TES backend storage configuration value `outputs` is required"),
1019 }
1020
1021 Ok(())
1022 }
1023
1024 pub fn redact(&mut self) {
1026 if let Some(auth) = &mut self.auth {
1027 auth.redact();
1028 }
1029 }
1030
1031 pub fn unredact(&mut self) {
1033 if let Some(auth) = &mut self.auth {
1034 auth.unredact();
1035 }
1036 }
1037}
1038
1039#[cfg(test)]
1040mod test {
1041 use pretty_assertions::assert_eq;
1042
1043 use super::*;
1044
1045 #[test]
1046 fn redacted_secret() {
1047 let mut secret: SecretString = "secret".into();
1048
1049 assert_eq!(
1050 serde_json::to_string(&secret).unwrap(),
1051 format!(r#""{REDACTED}""#)
1052 );
1053
1054 secret.unredact();
1055 assert_eq!(serde_json::to_string(&secret).unwrap(), r#""secret""#);
1056
1057 secret.redact();
1058 assert_eq!(
1059 serde_json::to_string(&secret).unwrap(),
1060 format!(r#""{REDACTED}""#)
1061 );
1062 }
1063
1064 #[test]
1065 fn redacted_config() {
1066 let config = Config {
1067 backends: [
1068 (
1069 "first".to_string(),
1070 BackendConfig::Tes(
1071 TesBackendConfig {
1072 auth: Some(TesBackendAuthConfig::Basic(BasicAuthConfig {
1073 username: "foo".into(),
1074 password: "secret".into(),
1075 })),
1076 ..Default::default()
1077 }
1078 .into(),
1079 ),
1080 ),
1081 (
1082 "second".to_string(),
1083 BackendConfig::Tes(
1084 TesBackendConfig {
1085 auth: Some(TesBackendAuthConfig::Bearer(BearerAuthConfig {
1086 token: "secret".into(),
1087 })),
1088 ..Default::default()
1089 }
1090 .into(),
1091 ),
1092 ),
1093 ]
1094 .into(),
1095 storage: StorageConfig {
1096 azure: AzureStorageConfig {
1097 auth: [("foo".into(), [("bar".into(), "secret".into())].into())].into(),
1098 },
1099 s3: S3StorageConfig {
1100 auth: Some(S3StorageAuthConfig {
1101 access_key_id: "foo".into(),
1102 secret_access_key: "secret".into(),
1103 }),
1104 ..Default::default()
1105 },
1106 google: GoogleStorageConfig {
1107 auth: Some(GoogleStorageAuthConfig {
1108 access_key: "foo".into(),
1109 secret: "secret".into(),
1110 }),
1111 },
1112 },
1113 ..Default::default()
1114 };
1115
1116 let json = serde_json::to_string_pretty(&config).unwrap();
1117 assert!(json.contains("secret"), "`{json}` contains a secret");
1118 }
1119
1120 #[test]
1121 fn test_config_validate() {
1122 let mut config = Config::default();
1124 config.task.retries = Some(1000000);
1125 assert_eq!(
1126 config.validate().unwrap_err().to_string(),
1127 "configuration value `task.retries` cannot exceed 100"
1128 );
1129
1130 let mut config = Config::default();
1132 config.workflow.scatter.concurrency = Some(0);
1133 assert_eq!(
1134 config.validate().unwrap_err().to_string(),
1135 "configuration value `workflow.scatter.concurrency` cannot be zero"
1136 );
1137
1138 let config = Config {
1140 backend: Some("foo".into()),
1141 ..Default::default()
1142 };
1143 assert_eq!(
1144 config.validate().unwrap_err().to_string(),
1145 "a backend named `foo` is not present in the configuration"
1146 );
1147 let config = Config {
1148 backend: Some("bar".into()),
1149 backends: [("foo".to_string(), BackendConfig::default())].into(),
1150 ..Default::default()
1151 };
1152 assert_eq!(
1153 config.validate().unwrap_err().to_string(),
1154 "a backend named `bar` is not present in the configuration"
1155 );
1156
1157 let config = Config {
1159 backends: [("foo".to_string(), BackendConfig::default())].into(),
1160 ..Default::default()
1161 };
1162 config.validate().expect("config should validate");
1163
1164 let config = Config {
1166 backends: [(
1167 "default".to_string(),
1168 BackendConfig::Local(LocalBackendConfig {
1169 cpu: Some(0),
1170 ..Default::default()
1171 }),
1172 )]
1173 .into(),
1174 ..Default::default()
1175 };
1176 assert_eq!(
1177 config.validate().unwrap_err().to_string(),
1178 "local backend configuration value `cpu` cannot be zero"
1179 );
1180 let config = Config {
1181 backends: [(
1182 "default".to_string(),
1183 BackendConfig::Local(LocalBackendConfig {
1184 cpu: Some(10000000),
1185 ..Default::default()
1186 }),
1187 )]
1188 .into(),
1189 ..Default::default()
1190 };
1191 assert!(config.validate().unwrap_err().to_string().starts_with(
1192 "local backend configuration value `cpu` cannot exceed the virtual CPUs available to \
1193 the host"
1194 ));
1195
1196 let config = Config {
1198 backends: [(
1199 "default".to_string(),
1200 BackendConfig::Local(LocalBackendConfig {
1201 memory: Some("0 GiB".to_string()),
1202 ..Default::default()
1203 }),
1204 )]
1205 .into(),
1206 ..Default::default()
1207 };
1208 assert_eq!(
1209 config.validate().unwrap_err().to_string(),
1210 "local backend configuration value `memory` cannot be zero"
1211 );
1212 let config = Config {
1213 backends: [(
1214 "default".to_string(),
1215 BackendConfig::Local(LocalBackendConfig {
1216 memory: Some("100 meows".to_string()),
1217 ..Default::default()
1218 }),
1219 )]
1220 .into(),
1221 ..Default::default()
1222 };
1223 assert_eq!(
1224 config.validate().unwrap_err().to_string(),
1225 "local backend configuration value `memory` has invalid value `100 meows`"
1226 );
1227
1228 let config = Config {
1229 backends: [(
1230 "default".to_string(),
1231 BackendConfig::Local(LocalBackendConfig {
1232 memory: Some("1000 TiB".to_string()),
1233 ..Default::default()
1234 }),
1235 )]
1236 .into(),
1237 ..Default::default()
1238 };
1239 assert!(config.validate().unwrap_err().to_string().starts_with(
1240 "local backend configuration value `memory` cannot exceed the total memory of the host"
1241 ));
1242
1243 let config = Config {
1245 backends: [(
1246 "default".to_string(),
1247 BackendConfig::Tes(Default::default()),
1248 )]
1249 .into(),
1250 ..Default::default()
1251 };
1252 assert_eq!(
1253 config.validate().unwrap_err().to_string(),
1254 "TES backend configuration value `url` is required"
1255 );
1256
1257 let config = Config {
1259 backends: [(
1260 "default".to_string(),
1261 BackendConfig::Tes(
1262 TesBackendConfig {
1263 url: Some("http://example.com".parse().unwrap()),
1264 inputs: Some("http://example.com".parse().unwrap()),
1265 outputs: Some("http://example.com".parse().unwrap()),
1266 ..Default::default()
1267 }
1268 .into(),
1269 ),
1270 )]
1271 .into(),
1272 ..Default::default()
1273 };
1274 assert_eq!(
1275 config.validate().unwrap_err().to_string(),
1276 "TES backend configuration value `url` has invalid value `http://example.com/`: URL \
1277 must use a HTTPS scheme"
1278 );
1279
1280 let config = Config {
1282 backends: [(
1283 "default".to_string(),
1284 BackendConfig::Tes(
1285 TesBackendConfig {
1286 url: Some("http://example.com".parse().unwrap()),
1287 inputs: Some("http://example.com".parse().unwrap()),
1288 outputs: Some("http://example.com".parse().unwrap()),
1289 insecure: true,
1290 ..Default::default()
1291 }
1292 .into(),
1293 ),
1294 )]
1295 .into(),
1296 ..Default::default()
1297 };
1298 config.validate().expect("configuration should validate");
1299
1300 let mut config = Config::default();
1301 config.http.parallelism = Some(0);
1302 assert_eq!(
1303 config.validate().unwrap_err().to_string(),
1304 "configuration value `http.parallelism` cannot be zero"
1305 );
1306
1307 let mut config = Config::default();
1308 config.http.parallelism = Some(5);
1309 assert!(
1310 config.validate().is_ok(),
1311 "should pass for valid configuration"
1312 );
1313
1314 let mut config = Config::default();
1315 config.http.parallelism = None;
1316 assert!(config.validate().is_ok(), "should pass for default (None)");
1317 }
1318}