1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::{KubernetesCloudReference, KubernetesClusterOwnership};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase")]
14pub struct AwsManagementConfig {
15 pub managing_role_arn: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[serde(rename_all = "camelCase")]
23pub struct GcpManagementConfig {
24 pub service_account_email: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub federation_trust: Option<crate::GcpFederationTrust>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35#[serde(rename_all = "camelCase")]
36pub struct AzureManagementConfig {
37 pub managing_tenant_id: String,
39 pub oidc_issuer: String,
41 pub oidc_subject: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51#[serde(rename_all = "camelCase", tag = "platform")]
52pub enum ManagementConfig {
53 Aws(AwsManagementConfig),
55 Gcp(GcpManagementConfig),
57 Azure(AzureManagementConfig),
59 Kubernetes,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
80#[serde(rename_all = "camelCase", tag = "type")]
81pub enum NetworkSettings {
82 #[serde(rename = "use-default")]
96 UseDefault,
97
98 #[serde(rename = "create")]
105 Create {
106 #[serde(skip_serializing_if = "Option::is_none")]
109 cidr: Option<String>,
110
111 #[serde(default = "default_availability_zones")]
113 availability_zones: u8,
114 },
115
116 #[serde(rename = "byo-vpc-aws")]
121 ByoVpcAws {
122 vpc_id: String,
124 #[serde(deserialize_with = "crate::network_ids::deserialize_ids")]
126 public_subnet_ids: Vec<String>,
127 #[serde(deserialize_with = "crate::network_ids::deserialize_ids")]
129 private_subnet_ids: Vec<String>,
130 #[serde(default)]
132 #[serde(deserialize_with = "crate::network_ids::deserialize_ids")]
133 security_group_ids: Vec<String>,
134 },
135
136 #[serde(rename = "byo-vpc-gcp")]
141 ByoVpcGcp {
142 network_name: String,
144 subnet_name: String,
146 region: String,
148 },
149
150 #[serde(rename = "byo-vnet-azure")]
155 ByoVnetAzure {
156 vnet_resource_id: String,
158 public_subnet_name: String,
160 private_subnet_name: String,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 application_gateway_subnet_name: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
171 private_endpoint_subnet_name: Option<String>,
172 },
173}
174
175fn default_availability_zones() -> u8 {
176 2
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
185#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
186#[serde(rename_all = "camelCase")]
187pub struct ComputeSettings {
188 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
190 pub pools: HashMap<String, ComputePoolSelection>,
191 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
193 pub containers: HashMap<String, ContainerResourceSelection>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
198#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
199#[serde(rename_all = "camelCase", deny_unknown_fields)]
200pub struct ContainerResourceSelection {
201 #[serde(
203 default,
204 skip_serializing_if = "Option::is_none",
205 deserialize_with = "deserialize_selected_cpu"
206 )]
207 #[cfg_attr(feature = "openapi", schema(value_type = Option<f64>))]
208 pub cpu: Option<serde_json::Number>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub memory: Option<String>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
215 #[serde(deserialize_with = "deserialize_selected_volume_size")]
216 pub volume_size_gib: Option<u32>,
217}
218
219fn deserialize_selected_volume_size<'de, D: serde::Deserializer<'de>>(
220 deserializer: D,
221) -> std::result::Result<Option<u32>, D::Error> {
222 match Option::<serde_json::Value>::deserialize(deserializer)? {
224 None => Ok(None),
225 Some(serde_json::Value::Number(number)) => number
226 .as_u64()
227 .and_then(|number| u32::try_from(number).ok())
228 .map(Some)
229 .ok_or_else(|| {
230 serde::de::Error::custom("volumeSizeGib must be a whole positive capacity")
231 }),
232 Some(serde_json::Value::String(number)) => {
233 number.parse().map(Some).map_err(serde::de::Error::custom)
234 }
235 Some(_) => Err(serde::de::Error::custom(
236 "volumeSizeGib must be a whole positive capacity",
237 )),
238 }
239}
240
241fn deserialize_selected_cpu<'de, D: serde::Deserializer<'de>>(
242 deserializer: D,
243) -> std::result::Result<Option<serde_json::Number>, D::Error> {
244 match Option::<serde_json::Value>::deserialize(deserializer)? {
246 None => Ok(None),
247 Some(serde_json::Value::Number(number)) => Ok(Some(number)),
248 Some(serde_json::Value::String(number)) => {
249 number.parse().map(Some).map_err(serde::de::Error::custom)
250 }
251 Some(_) => Err(serde::de::Error::custom("CPU must be a number")),
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
257#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
258#[serde(rename_all = "camelCase")]
259pub struct FailureDomainSelection {
260 pub spread: u8,
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub selected_failure_domains: Vec<String>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
270#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
271#[serde(rename_all = "camelCase", tag = "mode")]
272pub enum ComputePoolSelection {
273 Fixed {
275 machines: u32,
277 #[serde(
279 default,
280 skip_serializing_if = "Option::is_none",
281 deserialize_with = "deserialize_machine_override"
282 )]
283 machine: Option<String>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
286 failure_domains: Option<FailureDomainSelection>,
287 },
288 Autoscale {
290 min: u32,
292 max: u32,
294 #[serde(
296 default,
297 skip_serializing_if = "Option::is_none",
298 deserialize_with = "deserialize_machine_override"
299 )]
300 machine: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 failure_domains: Option<FailureDomainSelection>,
304 },
305}
306
307fn deserialize_machine_override<'de, D: serde::Deserializer<'de>>(
308 deserializer: D,
309) -> std::result::Result<Option<String>, D::Error> {
310 Ok(Option::<String>::deserialize(deserializer)?.filter(|machine| !machine.is_empty()))
312}
313
314impl ComputePoolSelection {
315 pub fn machine(&self) -> Option<&str> {
317 match self {
318 Self::Fixed { machine, .. } | Self::Autoscale { machine, .. } => machine.as_deref(),
319 }
320 }
321
322 pub fn failure_domains(&self) -> Option<&FailureDomainSelection> {
324 match self {
325 Self::Fixed {
326 failure_domains, ..
327 }
328 | Self::Autoscale {
329 failure_domains, ..
330 } => failure_domains.as_ref(),
331 }
332 }
333
334 pub fn min_size(&self) -> u32 {
336 match self {
337 Self::Fixed { machines, .. } => *machines,
338 Self::Autoscale { min, .. } => *min,
339 }
340 }
341
342 pub fn max_size(&self) -> u32 {
344 match self {
345 Self::Fixed { machines, .. } => *machines,
346 Self::Autoscale { max, .. } => *max,
347 }
348 }
349
350 pub fn validate(&self) -> std::result::Result<(), String> {
352 if self
353 .failure_domains()
354 .is_some_and(|selection| selection.spread == 0)
355 {
356 return Err("failure-domain spread must be at least one".to_string());
357 }
358 if self.failure_domains().is_some_and(|selection| {
359 !selection.selected_failure_domains.is_empty()
360 && selection.selected_failure_domains.len() != usize::from(selection.spread)
361 }) {
362 return Err("selected failure domains must match the requested spread".to_string());
363 }
364 if self.failure_domains().is_some_and(|selection| {
365 selection.selected_failure_domains.len()
366 != selection
367 .selected_failure_domains
368 .iter()
369 .collect::<std::collections::HashSet<_>>()
370 .len()
371 }) {
372 return Err("selected failure domains must be unique".to_string());
373 }
374 match self {
375 Self::Fixed { machines, .. } => {
376 if *machines == 0 {
377 return Err("fixed compute pools must select at least one machine".to_string());
378 }
379 if let Some(failure_domains) = self.failure_domains() {
380 if *machines < u32::from(failure_domains.spread) {
381 return Err(format!(
382 "fixed compute pool machines ({machines}) must be at least failure-domain spread ({})",
383 failure_domains.spread
384 ));
385 }
386 }
387 }
388 Self::Autoscale { min, max, .. } => {
389 if min > max {
390 return Err(format!(
391 "autoscaling compute pool minimum ({min}) cannot exceed maximum ({max})"
392 ));
393 }
394 if let Some(failure_domains) = self.failure_domains() {
395 let spread = u32::from(failure_domains.spread);
396 if *max < spread {
397 return Err(format!(
398 "autoscaling compute pool maximum ({max}) must be at least failure-domain spread ({spread})"
399 ));
400 }
401 if *min < spread {
402 return Err(format!(
403 "autoscaling compute pool minimum ({min}) must be at least failure-domain spread ({spread})"
404 ));
405 }
406 }
407 }
408 }
409 Ok(())
410 }
411}
412
413#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
415#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
416#[serde(rename_all = "camelCase")]
417pub enum DeploymentModel {
418 #[default]
421 Push,
422 Pull,
425}
426
427#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
429#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
430#[serde(rename_all = "kebab-case")]
431pub enum UpdatesMode {
432 #[default]
434 Auto,
435 ApprovalRequired,
437}
438
439#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
441#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
442#[serde(rename_all = "kebab-case")]
443pub enum TelemetryMode {
444 Off,
446 #[default]
448 Auto,
449 ApprovalRequired,
451}
452
453impl TelemetryMode {
454 pub fn is_enabled(&self) -> bool {
456 !matches!(self, TelemetryMode::Off)
457 }
458}
459
460#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
462#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
463#[serde(rename_all = "kebab-case")]
464pub enum HeartbeatsMode {
465 Off,
467 #[default]
469 On,
470}
471
472impl HeartbeatsMode {
473 pub fn is_enabled(&self) -> bool {
475 matches!(self, HeartbeatsMode::On)
476 }
477}
478
479#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
481#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
482#[serde(rename_all = "camelCase")]
483pub enum EndpointAccess {
484 #[default]
486 Internet,
487 Private,
489}
490
491impl EndpointAccess {
492 pub fn as_str(self) -> &'static str {
494 match self {
495 Self::Internet => "internet",
496 Self::Private => "private",
497 }
498 }
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
506#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
507#[serde(rename_all = "camelCase")]
508pub struct DomainSettings {
509 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub custom_domains: Option<HashMap<String, CustomDomainConfig>>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub public_endpoint_target: Option<PublicEndpointTargetSettings>,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
523#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
524#[serde(rename_all = "camelCase", tag = "mode")]
525pub enum PublicEndpointTargetSettings {
526 MachineAddresses,
528 #[serde(rename_all = "camelCase")]
530 LoadBalancer {
531 cname_target: String,
533 },
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
538#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
539#[serde(rename_all = "camelCase")]
540pub struct CustomDomainConfig {
541 pub domain: String,
543 pub certificate: CustomCertificateConfig,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
549#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
550#[serde(rename_all = "camelCase")]
551pub struct CustomCertificateConfig {
552 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub aws: Option<AwsCustomCertificateConfig>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub gcp: Option<GcpCustomCertificateConfig>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub azure: Option<AzureCustomCertificateConfig>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub kubernetes: Option<KubernetesCustomCertificateConfig>,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
567#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
568#[serde(rename_all = "camelCase")]
569pub struct AwsCustomCertificateConfig {
570 pub certificate_arn: String,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
574#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
575#[serde(rename_all = "camelCase")]
576pub struct GcpCustomCertificateConfig {
577 pub certificate_name: String,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
581#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
582#[serde(rename_all = "camelCase")]
583pub struct AzureCustomCertificateConfig {
584 pub key_vault_certificate_id: String,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub key_vault_resource_id: Option<String>,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
590#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
591#[serde(rename_all = "camelCase")]
592pub struct KubernetesCustomCertificateConfig {
593 pub tls_secret_ref: KubernetesTlsSecretRef,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
605#[serde(rename_all = "camelCase")]
606pub struct KubernetesSettings {
607 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub cluster: Option<KubernetesClusterSettings>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub exposure: Option<KubernetesExposureSettings>,
613}
614
615#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
617#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
618#[serde(rename_all = "camelCase")]
619pub struct KubernetesClusterSettings {
620 pub ownership: KubernetesClusterOwnership,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub namespace: Option<String>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub cloud: Option<KubernetesCloudReference>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
633#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
634#[serde(rename_all = "camelCase", tag = "mode")]
635pub enum KubernetesExposureSettings {
636 Disabled,
638 Generated {
640 route: KubernetesRouteProfile,
642 certificate: KubernetesCertificateMode,
644 },
645 Custom {
647 domain: String,
649 route: KubernetesRouteProfile,
651 certificate: KubernetesCertificateMode,
653 },
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
658#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
659#[serde(rename_all = "camelCase", tag = "routeApi")]
660pub enum KubernetesRouteProfile {
661 Ingress(KubernetesIngressRouteProfile),
663 Gateway(KubernetesGatewayRouteProfile),
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
669#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
670#[serde(rename_all = "camelCase")]
671pub struct KubernetesIngressRouteProfile {
672 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub controller: Option<String>,
675 pub ingress_class_name: String,
677 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
679 pub labels: HashMap<String, String>,
680 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
682 pub annotations: HashMap<String, String>,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub provider: Option<KubernetesRouteProviderOptions>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
690#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
691#[serde(rename_all = "camelCase")]
692pub struct KubernetesGatewayRouteProfile {
693 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub controller: Option<String>,
696 pub gateway_class_name: String,
698 pub listener_port: u16,
700 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
702 pub labels: HashMap<String, String>,
703 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
705 pub annotations: HashMap<String, String>,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
708 pub provider: Option<KubernetesRouteProviderOptions>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
713#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
714#[serde(rename_all = "camelCase", tag = "provider")]
715pub enum KubernetesRouteProviderOptions {
716 #[serde(rename_all = "camelCase")]
718 AwsAlb {
719 scheme: String,
721 target_type: String,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
725 ip_address_type: Option<String>,
726 #[serde(default, skip_serializing_if = "Vec::is_empty")]
728 subnet_ids: Vec<String>,
729 },
730 #[serde(rename_all = "camelCase")]
732 GkeGateway {
733 #[serde(default, skip_serializing_if = "Option::is_none")]
735 static_address_name: Option<String>,
736 },
737 #[serde(rename_all = "camelCase")]
739 AzureApplicationGatewayForContainers {
740 #[serde(default, skip_serializing_if = "Option::is_none")]
742 alb_namespace: Option<String>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 alb_name: Option<String>,
746 frontend: String,
748 },
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
753#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
754#[serde(rename_all = "camelCase", tag = "mode")]
755pub enum KubernetesCertificateMode {
756 #[serde(rename_all = "camelCase")]
758 ManagedAcmImport {
759 #[serde(default, skip_serializing_if = "Option::is_none")]
761 region: Option<String>,
762 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
764 tags: HashMap<String, String>,
765 },
766 #[serde(rename_all = "camelCase")]
768 AwsAcmArn {
769 certificate_arn: String,
771 },
772 #[serde(rename_all = "camelCase")]
774 ManagedTlsSecret {
775 secret_name_template: String,
777 },
778 TlsSecretRef(KubernetesTlsSecretRef),
780 None,
782}
783
784#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
786#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
787#[serde(rename_all = "camelCase")]
788pub struct KubernetesTlsSecretRef {
789 pub secret_name: String,
791 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub namespace: Option<String>,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
805#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
806#[serde(rename_all = "camelCase")]
807pub struct StackSettings {
808 #[serde(default, skip_serializing_if = "is_manual_setup_management")]
811 pub setup_management: SetupManagement,
812 #[serde(default, skip_serializing_if = "Vec::is_empty")]
816 pub quota_warning_overrides: Vec<String>,
817
818 #[serde(default)]
821 pub endpoint_access: EndpointAccess,
822
823 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub network: Option<NetworkSettings>,
831
832 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub domains: Option<DomainSettings>,
835
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub kubernetes: Option<KubernetesSettings>,
839
840 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub compute: Option<ComputeSettings>,
847
848 #[serde(default, skip_serializing_if = "Option::is_none")]
854 #[cfg_attr(
855 feature = "openapi",
856 schema(value_type = Option<HashMap<String, HashMap<String, String>>>)
857 )]
858 pub public_endpoints: Option<crate::PublicEndpointUrls>,
859
860 #[serde(default, skip_serializing_if = "is_default_deployment_model")]
869 pub deployment_model: DeploymentModel,
870
871 #[serde(default, skip_serializing_if = "is_default_updates_mode")]
875 pub updates: UpdatesMode,
876
877 #[serde(default, skip_serializing_if = "is_default_telemetry_mode")]
882 pub telemetry: TelemetryMode,
883
884 #[serde(default, skip_serializing_if = "is_default_heartbeats_mode")]
888 pub heartbeats: HeartbeatsMode,
889
890 #[serde(default, skip_serializing_if = "Option::is_none")]
895 pub external_bindings: Option<crate::ExternalBindings>,
896}
897
898#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
900#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
901#[serde(rename_all = "camelCase")]
902pub enum SetupManagement {
903 #[default]
905 Manual,
906 Automatic,
908}
909
910fn is_manual_setup_management(mode: &SetupManagement) -> bool {
911 *mode == SetupManagement::Manual
912}
913
914fn is_default_deployment_model(model: &DeploymentModel) -> bool {
915 *model == DeploymentModel::default()
916}
917
918fn is_default_updates_mode(mode: &UpdatesMode) -> bool {
919 *mode == UpdatesMode::default()
920}
921
922fn is_default_telemetry_mode(mode: &TelemetryMode) -> bool {
923 *mode == TelemetryMode::default()
924}
925
926fn is_default_heartbeats_mode(mode: &HeartbeatsMode) -> bool {
927 *mode == HeartbeatsMode::default()
928}
929
930#[cfg(test)]
931mod failure_domain_tests {
932 use super::*;
933
934 #[test]
935 fn old_compute_selection_deserializes_without_topology() {
936 let selection: ComputePoolSelection = serde_json::from_value(serde_json::json!({
937 "mode": "fixed",
938 "machines": 2,
939 "machine": "m7i.xlarge"
940 }))
941 .expect("existing selection should deserialize");
942 assert!(selection.failure_domains().is_none());
943 }
944
945 #[test]
946 fn rejects_duplicate_concrete_failure_domains() {
947 let selection = ComputePoolSelection::Fixed {
948 machines: 2,
949 machine: Some("m7i.xlarge".to_string()),
950 failure_domains: Some(FailureDomainSelection {
951 spread: 2,
952 selected_failure_domains: vec!["us-west-2a".to_string(); 2],
953 }),
954 };
955 assert_eq!(
956 selection.validate(),
957 Err("selected failure domains must be unique".to_string())
958 );
959 }
960
961 #[test]
962 fn fixed_pool_must_have_one_machine_per_failure_domain() {
963 let invalid = ComputePoolSelection::Fixed {
964 machines: 1,
965 machine: None,
966 failure_domains: Some(FailureDomainSelection {
967 spread: 2,
968 selected_failure_domains: Vec::new(),
969 }),
970 };
971 assert_eq!(
972 invalid.validate(),
973 Err(
974 "fixed compute pool machines (1) must be at least failure-domain spread (2)"
975 .to_string()
976 )
977 );
978
979 let valid = ComputePoolSelection::Fixed {
980 machines: 2,
981 machine: None,
982 failure_domains: Some(FailureDomainSelection {
983 spread: 2,
984 selected_failure_domains: Vec::new(),
985 }),
986 };
987 assert_eq!(valid.validate(), Ok(()));
988 }
989
990 #[test]
991 fn autoscaling_pool_bounds_must_cover_every_failure_domain() {
992 let invalid_max = ComputePoolSelection::Autoscale {
993 min: 1,
994 max: 1,
995 machine: None,
996 failure_domains: Some(FailureDomainSelection {
997 spread: 2,
998 selected_failure_domains: Vec::new(),
999 }),
1000 };
1001 assert_eq!(
1002 invalid_max.validate(),
1003 Err(
1004 "autoscaling compute pool maximum (1) must be at least failure-domain spread (2)"
1005 .to_string()
1006 )
1007 );
1008
1009 let invalid_min = ComputePoolSelection::Autoscale {
1010 min: 1,
1011 max: 3,
1012 machine: None,
1013 failure_domains: Some(FailureDomainSelection {
1014 spread: 2,
1015 selected_failure_domains: Vec::new(),
1016 }),
1017 };
1018 assert_eq!(
1019 invalid_min.validate(),
1020 Err(
1021 "autoscaling compute pool minimum (1) must be at least failure-domain spread (2)"
1022 .to_string()
1023 )
1024 );
1025
1026 let valid = ComputePoolSelection::Autoscale {
1027 min: 2,
1028 max: 2,
1029 machine: None,
1030 failure_domains: Some(FailureDomainSelection {
1031 spread: 2,
1032 selected_failure_domains: Vec::new(),
1033 }),
1034 };
1035 assert_eq!(valid.validate(), Ok(()));
1036 }
1037
1038 #[test]
1039 fn machine_public_endpoints_round_trip_without_rewriting_urls() {
1040 let settings: StackSettings = serde_json::from_value(serde_json::json!({
1041 "publicEndpoints": {
1042 "loader": {
1043 "api": "https://loader.compute.example.com",
1044 "shares": "https://shares.compute.example.com",
1045 "webhooks": "https://webhooks.compute.example.com"
1046 }
1047 }
1048 }))
1049 .expect("stack settings should deserialize");
1050
1051 assert_eq!(
1052 serde_json::to_value(settings).expect("stack settings should serialize")
1053 ["publicEndpoints"]["loader"]["shares"],
1054 "https://shares.compute.example.com"
1055 );
1056 }
1057}