1use serde::{Deserialize, Serialize};
2
3use crate::adapter::{AdapterFamily, MultiTargetKind, UnifiedTargetSelection};
4use crate::config::{DeployerConfig, Provider};
5use crate::contract::DeployerCapability;
6use crate::error::{DeployerError, Result};
7use crate::extension_sources::{
8 DeploymentExtensionSourceOptions, list_pack_deployment_extension_contracts,
9};
10use crate::multi_target::OperationResult;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum BuiltinBackendId {
15 Terraform,
16 K8sRaw,
17 Helm,
18 Aws,
19 Azure,
20 Gcp,
21 JujuK8s,
22 JujuMachine,
23 Operator,
24 Serverless,
25 Snap,
26 Desktop,
27 SingleVm,
28}
29
30impl BuiltinBackendId {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Terraform => "terraform",
34 Self::K8sRaw => "k8s_raw",
35 Self::Helm => "helm",
36 Self::Aws => "aws",
37 Self::Azure => "azure",
38 Self::Gcp => "gcp",
39 Self::JujuK8s => "juju_k8s",
40 Self::JujuMachine => "juju_machine",
41 Self::Operator => "operator",
42 Self::Serverless => "serverless",
43 Self::Snap => "snap",
44 Self::Desktop => "desktop",
45 Self::SingleVm => "single_vm",
46 }
47 }
48
49 pub fn handler_matches(self, handler: Option<&str>) -> bool {
54 match self {
55 Self::Desktop => matches!(handler, None | Some("docker-compose") | Some("podman")),
56 _ => handler.is_none(),
57 }
58 }
59}
60
61impl std::str::FromStr for BuiltinBackendId {
62 type Err = UnknownBuiltinBackendStr;
63
64 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
65 Ok(match s {
66 "terraform" => Self::Terraform,
67 "k8s_raw" => Self::K8sRaw,
68 "helm" => Self::Helm,
69 "aws" => Self::Aws,
70 "azure" => Self::Azure,
71 "gcp" => Self::Gcp,
72 "juju_k8s" => Self::JujuK8s,
73 "juju_machine" => Self::JujuMachine,
74 "operator" => Self::Operator,
75 "serverless" => Self::Serverless,
76 "snap" => Self::Snap,
77 "desktop" => Self::Desktop,
78 "single_vm" => Self::SingleVm,
79 other => return Err(UnknownBuiltinBackendStr(other.to_string())),
80 })
81 }
82}
83
84#[derive(Debug, thiserror::Error)]
85#[error("unknown builtin backend id: '{0}'")]
86pub struct UnknownBuiltinBackendStr(pub String);
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum BuiltinBackendExecutionKind {
91 Common,
92 Executable,
93 Cloud,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum BuiltinBackendHandlerId {
99 Terraform,
100 K8sRaw,
101 Helm,
102 Aws,
103 Azure,
104 Gcp,
105 JujuK8s,
106 JujuMachine,
107 Operator,
108 Serverless,
109 Snap,
110 Desktop,
111 SingleVm,
112}
113
114impl BuiltinBackendHandlerId {
115 pub fn as_str(self) -> &'static str {
116 match self {
117 Self::Terraform => "terraform",
118 Self::K8sRaw => "k8s_raw",
119 Self::Helm => "helm",
120 Self::Aws => "aws",
121 Self::Azure => "azure",
122 Self::Gcp => "gcp",
123 Self::JujuK8s => "juju_k8s",
124 Self::JujuMachine => "juju_machine",
125 Self::Operator => "operator",
126 Self::Serverless => "serverless",
127 Self::Snap => "snap",
128 Self::Desktop => "desktop",
129 Self::SingleVm => "single_vm",
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum DeploymentExtensionKind {
137 Builtin,
138 Pack,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum DeploymentExtensionSourceKind {
144 Builtin,
145 Pack,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct DeploymentExtensionDescriptor {
150 pub id: String,
151 pub kind: DeploymentExtensionKind,
152 pub target: UnifiedTargetSelection,
153 pub summary: String,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct BuiltinBackendDescriptor {
158 pub backend_id: BuiltinBackendId,
159 pub execution_kind: BuiltinBackendExecutionKind,
160 pub handler_id: BuiltinBackendHandlerId,
161 pub extension: DeploymentExtensionDescriptor,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct BuiltinExtensionBackendDescriptor {
166 pub backend_id: BuiltinBackendId,
167 pub execution_kind: BuiltinBackendExecutionKind,
168 pub handler_id: BuiltinBackendHandlerId,
169 pub supported_capabilities: Vec<DeployerCapability>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct BuiltinHandlerDescriptor {
174 pub handler_id: BuiltinBackendHandlerId,
175 pub backend_id: BuiltinBackendId,
176 pub execution_kind: BuiltinBackendExecutionKind,
177 pub supported_capabilities: Vec<DeployerCapability>,
178 pub extension: DeploymentExtensionDescriptor,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
182pub struct BuiltinExtensionDescriptor {
183 pub extension: DeploymentExtensionDescriptor,
184 pub provider: Option<Provider>,
185 pub aliases: Vec<String>,
186 pub backends: Vec<BuiltinExtensionBackendDescriptor>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct DeploymentHandlerDescriptor {
191 pub id: String,
192 pub execution_kind: BuiltinBackendExecutionKind,
193 pub supported_capabilities: Vec<DeployerCapability>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
197pub struct DeploymentExtensionContract {
198 pub source: DeploymentExtensionSourceKind,
199 pub extension: DeploymentExtensionDescriptor,
200 pub provider: Option<Provider>,
201 pub aliases: Vec<String>,
202 pub handlers: Vec<DeploymentHandlerDescriptor>,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206struct BuiltinBackendBinding {
207 backend_id: BuiltinBackendId,
208 execution_kind: BuiltinBackendExecutionKind,
209 handler_id: BuiltinBackendHandlerId,
210 supported_capabilities: &'static [DeployerCapability],
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214struct BuiltinExtensionRegistration {
215 provider: Option<Provider>,
216 aliases: &'static [&'static str],
217 extension_id: &'static str,
218 target: UnifiedTargetSelection,
219 summary: &'static str,
220 backends: &'static [BuiltinBackendBinding],
221}
222
223const STANDARD_DEPLOYER_CAPABILITIES: &[DeployerCapability] = &[
224 DeployerCapability::Generate,
225 DeployerCapability::Plan,
226 DeployerCapability::Apply,
227 DeployerCapability::Destroy,
228 DeployerCapability::Status,
229 DeployerCapability::Rollback,
230];
231
232const GENERIC_EXECUTABLE_BACKENDS: &[BuiltinBackendBinding] = &[
233 BuiltinBackendBinding {
234 backend_id: BuiltinBackendId::Terraform,
235 execution_kind: BuiltinBackendExecutionKind::Executable,
236 handler_id: BuiltinBackendHandlerId::Terraform,
237 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
238 },
239 BuiltinBackendBinding {
240 backend_id: BuiltinBackendId::Serverless,
241 execution_kind: BuiltinBackendExecutionKind::Executable,
242 handler_id: BuiltinBackendHandlerId::Serverless,
243 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
244 },
245];
246
247const K8S_BACKENDS: &[BuiltinBackendBinding] = &[
248 BuiltinBackendBinding {
249 backend_id: BuiltinBackendId::K8sRaw,
250 execution_kind: BuiltinBackendExecutionKind::Common,
251 handler_id: BuiltinBackendHandlerId::K8sRaw,
252 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
253 },
254 BuiltinBackendBinding {
255 backend_id: BuiltinBackendId::Helm,
256 execution_kind: BuiltinBackendExecutionKind::Common,
257 handler_id: BuiltinBackendHandlerId::Helm,
258 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
259 },
260 BuiltinBackendBinding {
261 backend_id: BuiltinBackendId::JujuK8s,
262 execution_kind: BuiltinBackendExecutionKind::Executable,
263 handler_id: BuiltinBackendHandlerId::JujuK8s,
264 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
265 },
266 BuiltinBackendBinding {
267 backend_id: BuiltinBackendId::Operator,
268 execution_kind: BuiltinBackendExecutionKind::Executable,
269 handler_id: BuiltinBackendHandlerId::Operator,
270 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
271 },
272];
273
274const LOCAL_BACKENDS: &[BuiltinBackendBinding] = &[
275 BuiltinBackendBinding {
276 backend_id: BuiltinBackendId::JujuMachine,
277 execution_kind: BuiltinBackendExecutionKind::Executable,
278 handler_id: BuiltinBackendHandlerId::JujuMachine,
279 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
280 },
281 BuiltinBackendBinding {
282 backend_id: BuiltinBackendId::Snap,
283 execution_kind: BuiltinBackendExecutionKind::Executable,
284 handler_id: BuiltinBackendHandlerId::Snap,
285 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
286 },
287];
288
289const AWS_BACKENDS: &[BuiltinBackendBinding] = &[BuiltinBackendBinding {
290 backend_id: BuiltinBackendId::Aws,
291 execution_kind: BuiltinBackendExecutionKind::Cloud,
292 handler_id: BuiltinBackendHandlerId::Aws,
293 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
294}];
295
296const AZURE_BACKENDS: &[BuiltinBackendBinding] = &[BuiltinBackendBinding {
297 backend_id: BuiltinBackendId::Azure,
298 execution_kind: BuiltinBackendExecutionKind::Cloud,
299 handler_id: BuiltinBackendHandlerId::Azure,
300 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
301}];
302
303const GCP_BACKENDS: &[BuiltinBackendBinding] = &[BuiltinBackendBinding {
304 backend_id: BuiltinBackendId::Gcp,
305 execution_kind: BuiltinBackendExecutionKind::Cloud,
306 handler_id: BuiltinBackendHandlerId::Gcp,
307 supported_capabilities: STANDARD_DEPLOYER_CAPABILITIES,
308}];
309
310const SINGLE_VM_BACKENDS: &[BuiltinBackendBinding] = &[];
311
312const BUILTIN_EXTENSION_REGISTRATIONS: &[BuiltinExtensionRegistration] = &[
313 BuiltinExtensionRegistration {
314 provider: None,
315 aliases: &["single-vm", "single_vm"],
316 extension_id: "builtin.single_vm.core",
317 target: UnifiedTargetSelection::SingleVm,
318 summary: "Built-in single-vm deployment extension",
319 backends: SINGLE_VM_BACKENDS,
320 },
321 BuiltinExtensionRegistration {
322 provider: Some(Provider::Local),
323 aliases: &["local"],
324 extension_id: "builtin.multi_target.local",
325 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::Local),
326 summary: "Built-in local multi-target deployment extension",
327 backends: LOCAL_BACKENDS,
328 },
329 BuiltinExtensionRegistration {
330 provider: Some(Provider::Aws),
331 aliases: &["aws"],
332 extension_id: "builtin.multi_target.aws",
333 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::Aws),
334 summary: "Built-in AWS multi-target deployment extension",
335 backends: AWS_BACKENDS,
336 },
337 BuiltinExtensionRegistration {
338 provider: Some(Provider::Azure),
339 aliases: &["azure"],
340 extension_id: "builtin.multi_target.azure",
341 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::Azure),
342 summary: "Built-in Azure multi-target deployment extension",
343 backends: AZURE_BACKENDS,
344 },
345 BuiltinExtensionRegistration {
346 provider: Some(Provider::Gcp),
347 aliases: &["gcp"],
348 extension_id: "builtin.multi_target.gcp",
349 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::Gcp),
350 summary: "Built-in GCP multi-target deployment extension",
351 backends: GCP_BACKENDS,
352 },
353 BuiltinExtensionRegistration {
354 provider: Some(Provider::K8s),
355 aliases: &["k8s"],
356 extension_id: "builtin.multi_target.k8s",
357 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::K8s),
358 summary: "Built-in Kubernetes multi-target deployment extension",
359 backends: K8S_BACKENDS,
360 },
361 BuiltinExtensionRegistration {
362 provider: Some(Provider::Generic),
363 aliases: &["generic"],
364 extension_id: "builtin.multi_target.generic",
365 target: UnifiedTargetSelection::MultiTarget(MultiTargetKind::Generic),
366 summary: "Built-in generic multi-target deployment extension",
367 backends: GENERIC_EXECUTABLE_BACKENDS,
368 },
369];
370
371impl DeploymentExtensionDescriptor {
372 pub fn builtin(
373 id: impl Into<String>,
374 target: UnifiedTargetSelection,
375 summary: impl Into<String>,
376 ) -> Self {
377 Self {
378 id: id.into(),
379 kind: DeploymentExtensionKind::Builtin,
380 target,
381 summary: summary.into(),
382 }
383 }
384
385 pub fn adapter_family(&self) -> AdapterFamily {
386 self.target.adapter_family()
387 }
388}
389
390fn descriptor_from_registration(
391 registration: &BuiltinExtensionRegistration,
392) -> DeploymentExtensionDescriptor {
393 DeploymentExtensionDescriptor::builtin(
394 registration.extension_id,
395 registration.target,
396 registration.summary,
397 )
398}
399
400fn builtin_extension_descriptor_from_registration(
401 registration: &BuiltinExtensionRegistration,
402) -> BuiltinExtensionDescriptor {
403 BuiltinExtensionDescriptor {
404 extension: descriptor_from_registration(registration),
405 provider: registration.provider,
406 aliases: registration
407 .aliases
408 .iter()
409 .map(|alias| (*alias).to_string())
410 .collect(),
411 backends: registration
412 .backends
413 .iter()
414 .map(|binding| BuiltinExtensionBackendDescriptor {
415 backend_id: binding.backend_id,
416 execution_kind: binding.execution_kind,
417 handler_id: binding.handler_id,
418 supported_capabilities: binding.supported_capabilities.to_vec(),
419 })
420 .collect(),
421 }
422}
423
424fn builtin_handler_descriptor_from_parts(
425 registration: &BuiltinExtensionRegistration,
426 binding: &BuiltinBackendBinding,
427) -> BuiltinHandlerDescriptor {
428 BuiltinHandlerDescriptor {
429 handler_id: binding.handler_id,
430 backend_id: binding.backend_id,
431 execution_kind: binding.execution_kind,
432 supported_capabilities: binding.supported_capabilities.to_vec(),
433 extension: descriptor_from_registration(registration),
434 }
435}
436
437fn deployment_extension_contract_from_registration(
438 registration: &BuiltinExtensionRegistration,
439) -> DeploymentExtensionContract {
440 DeploymentExtensionContract {
441 source: DeploymentExtensionSourceKind::Builtin,
442 extension: descriptor_from_registration(registration),
443 provider: registration.provider,
444 aliases: registration
445 .aliases
446 .iter()
447 .map(|alias| (*alias).to_string())
448 .collect(),
449 handlers: registration
450 .backends
451 .iter()
452 .map(|binding| DeploymentHandlerDescriptor {
453 id: format!("builtin.{}", binding.handler_id.as_str()),
454 execution_kind: binding.execution_kind,
455 supported_capabilities: binding.supported_capabilities.to_vec(),
456 })
457 .collect(),
458 }
459}
460
461pub fn list_builtin_extensions() -> Vec<BuiltinExtensionDescriptor> {
462 BUILTIN_EXTENSION_REGISTRATIONS
463 .iter()
464 .map(builtin_extension_descriptor_from_registration)
465 .collect()
466}
467
468pub fn list_deployment_extension_contracts() -> Vec<DeploymentExtensionContract> {
469 BUILTIN_EXTENSION_REGISTRATIONS
470 .iter()
471 .map(deployment_extension_contract_from_registration)
472 .collect()
473}
474
475pub fn list_deployment_extension_contracts_from_sources() -> Vec<DeploymentExtensionContract> {
476 list_deployment_extension_contracts_from_sources_with_options(
477 &DeploymentExtensionSourceOptions::default(),
478 )
479}
480
481pub fn list_deployment_extension_contracts_from_sources_with_options(
482 options: &DeploymentExtensionSourceOptions,
483) -> Vec<DeploymentExtensionContract> {
484 let mut contracts = list_deployment_extension_contracts();
485 contracts.extend(list_pack_deployment_extension_contracts(options));
486 contracts
487}
488
489pub fn list_builtin_handlers() -> Vec<BuiltinHandlerDescriptor> {
490 BUILTIN_EXTENSION_REGISTRATIONS
491 .iter()
492 .flat_map(|registration| {
493 registration
494 .backends
495 .iter()
496 .map(move |binding| builtin_handler_descriptor_from_parts(registration, binding))
497 })
498 .collect()
499}
500
501pub fn resolve_builtin_extension_detail_for_provider(
502 provider: Provider,
503) -> Option<BuiltinExtensionDescriptor> {
504 BUILTIN_EXTENSION_REGISTRATIONS
505 .iter()
506 .find(|registration| registration.provider == Some(provider))
507 .map(builtin_extension_descriptor_from_registration)
508}
509
510pub fn resolve_deployment_extension_contract_for_provider(
511 provider: Provider,
512) -> Option<DeploymentExtensionContract> {
513 BUILTIN_EXTENSION_REGISTRATIONS
514 .iter()
515 .find(|registration| registration.provider == Some(provider))
516 .map(deployment_extension_contract_from_registration)
517}
518
519pub fn resolve_deployment_extension_contract_for_provider_from_sources(
520 provider: Provider,
521) -> Option<DeploymentExtensionContract> {
522 resolve_deployment_extension_contract_for_provider_from_sources_with_options(
523 provider,
524 &DeploymentExtensionSourceOptions::default(),
525 )
526}
527
528pub fn resolve_deployment_extension_contract_for_provider_from_sources_with_options(
529 provider: Provider,
530 options: &DeploymentExtensionSourceOptions,
531) -> Option<DeploymentExtensionContract> {
532 list_deployment_extension_contracts_from_sources_with_options(options)
533 .into_iter()
534 .find(|contract| contract.provider == Some(provider))
535}
536
537pub fn resolve_builtin_extension_detail_for_target_name(
538 target: &str,
539) -> Option<BuiltinExtensionDescriptor> {
540 BUILTIN_EXTENSION_REGISTRATIONS
541 .iter()
542 .find(|registration| {
543 registration
544 .aliases
545 .iter()
546 .any(|alias| alias.eq_ignore_ascii_case(target.trim()))
547 })
548 .map(builtin_extension_descriptor_from_registration)
549}
550
551pub fn resolve_deployment_extension_contract_for_target_name(
552 target: &str,
553) -> Option<DeploymentExtensionContract> {
554 BUILTIN_EXTENSION_REGISTRATIONS
555 .iter()
556 .find(|registration| {
557 registration
558 .aliases
559 .iter()
560 .any(|alias| alias.eq_ignore_ascii_case(target.trim()))
561 })
562 .map(deployment_extension_contract_from_registration)
563}
564
565pub fn resolve_deployment_extension_contract_for_target_name_from_sources(
566 target: &str,
567) -> Option<DeploymentExtensionContract> {
568 resolve_deployment_extension_contract_for_target_name_from_sources_with_options(
569 target,
570 &DeploymentExtensionSourceOptions::default(),
571 )
572}
573
574pub fn resolve_deployment_extension_contract_for_target_name_from_sources_with_options(
575 target: &str,
576 options: &DeploymentExtensionSourceOptions,
577) -> Option<DeploymentExtensionContract> {
578 list_deployment_extension_contracts_from_sources_with_options(options)
579 .into_iter()
580 .find(|contract| {
581 contract
582 .aliases
583 .iter()
584 .any(|alias| alias.eq_ignore_ascii_case(target.trim()))
585 })
586}
587
588pub fn resolve_builtin_handler_descriptor(
589 handler_id: BuiltinBackendHandlerId,
590) -> Option<BuiltinHandlerDescriptor> {
591 BUILTIN_EXTENSION_REGISTRATIONS
592 .iter()
593 .find_map(|registration| {
594 registration
595 .backends
596 .iter()
597 .find(|binding| binding.handler_id == handler_id)
598 .map(|binding| builtin_handler_descriptor_from_parts(registration, binding))
599 })
600}
601
602pub fn resolve_builtin_extension_for_provider(
603 provider: Provider,
604) -> Option<DeploymentExtensionDescriptor> {
605 resolve_builtin_extension_detail_for_provider(provider).map(|detail| detail.extension)
606}
607
608pub fn single_vm_builtin_extension() -> DeploymentExtensionDescriptor {
609 resolve_builtin_extension_for_target_name("single-vm")
610 .expect("single-vm extension registration must exist")
611}
612
613pub fn resolve_builtin_extension_for_target_name(
614 target: &str,
615) -> Option<DeploymentExtensionDescriptor> {
616 resolve_builtin_extension_detail_for_target_name(target).map(|detail| detail.extension)
617}
618
619pub fn resolve_builtin_backend_descriptor(
620 backend_id: BuiltinBackendId,
621) -> Option<BuiltinBackendDescriptor> {
622 let registration = BUILTIN_EXTENSION_REGISTRATIONS
623 .iter()
624 .find_map(|registration| {
625 registration
626 .backends
627 .iter()
628 .find(|binding| binding.backend_id == backend_id)
629 .map(|binding| (registration, binding))
630 })?;
631 Some(BuiltinBackendDescriptor {
632 backend_id: registration.1.backend_id,
633 execution_kind: registration.1.execution_kind,
634 handler_id: registration.1.handler_id,
635 extension: descriptor_from_registration(registration.0),
636 })
637}
638
639pub fn resolve_builtin_extension_for_config(
640 config: &DeployerConfig,
641) -> Option<DeploymentExtensionDescriptor> {
642 resolve_builtin_extension_for_provider(config.provider)
643}
644
645pub async fn run_builtin_extension(config: DeployerConfig) -> Result<OperationResult> {
646 let descriptor = resolve_builtin_extension_for_config(&config).ok_or_else(|| {
647 DeployerError::Other(format!(
648 "no built-in deployment extension registered for provider {}",
649 config.provider.as_str()
650 ))
651 })?;
652
653 match descriptor.target {
654 UnifiedTargetSelection::MultiTarget(_) => crate::multi_target::run(config).await,
655 UnifiedTargetSelection::SingleVm => Err(DeployerError::Other(
656 "single-vm execution must use the single-vm adapter path, not multi-target dispatch"
657 .to_string(),
658 )),
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::config::{DeployerRequest, OutputFormat};
666 use crate::contract::DeployerCapability;
667 use std::path::PathBuf;
668
669 #[test]
670 fn cloud_providers_resolve_to_builtin_multi_target_extensions() {
671 let aws = resolve_builtin_extension_for_provider(Provider::Aws).expect("aws extension");
672 assert_eq!(aws.id, "builtin.multi_target.aws");
673 assert_eq!(aws.kind, DeploymentExtensionKind::Builtin);
674 assert_eq!(aws.adapter_family(), AdapterFamily::MultiTarget);
675 }
676
677 #[test]
678 fn single_vm_extension_stays_in_single_vm_family() {
679 let descriptor = single_vm_builtin_extension();
680 assert_eq!(descriptor.id, "builtin.single_vm.core");
681 assert_eq!(descriptor.kind, DeploymentExtensionKind::Builtin);
682 assert_eq!(descriptor.adapter_family(), AdapterFamily::SingleVm);
683 }
684
685 #[test]
686 fn resolve_builtin_extension_for_config_uses_provider() {
687 let base = std::env::current_dir().unwrap().join("target/tmp-tests");
688 std::fs::create_dir_all(&base).unwrap();
689 let dir = tempfile::tempdir_in(&base).unwrap();
690
691 let request = DeployerRequest {
692 capability: DeployerCapability::Apply,
693 provider: Provider::Aws,
694 strategy: "iac-only".to_string(),
695 tenant: "demo".to_string(),
696 environment: Some("dev".to_string()),
697 pack_path: dir.path().to_path_buf(),
698 bundle_root: None,
699 bundle_source: None,
700 bundle_digest: None,
701 repo_registry_base: None,
702 store_registry_base: None,
703 providers_dir: PathBuf::from("providers/deployer"),
704 packs_dir: PathBuf::from("packs"),
705 provider_pack: None,
706 pack_id: None,
707 pack_version: None,
708 pack_digest: None,
709 distributor_url: None,
710 distributor_token: None,
711 preview: false,
712 dry_run: false,
713 execute_local: false,
714 output: OutputFormat::Json,
715 config_path: None,
716 allow_remote_in_offline: false,
717 deploy_pack_id_override: None,
718 deploy_flow_id_override: None,
719 };
720 let config = DeployerConfig::resolve(request).expect("config");
721 let descriptor = resolve_builtin_extension_for_config(&config).expect("descriptor");
722 assert_eq!(descriptor.id, "builtin.multi_target.aws");
723 }
724
725 #[test]
726 fn builtin_backend_descriptor_maps_backend_to_extension() {
727 let aws = resolve_builtin_backend_descriptor(BuiltinBackendId::Aws).expect("aws backend");
728 assert_eq!(aws.backend_id, BuiltinBackendId::Aws);
729 assert_eq!(aws.execution_kind, BuiltinBackendExecutionKind::Cloud);
730 assert_eq!(aws.handler_id, BuiltinBackendHandlerId::Aws);
731 assert_eq!(aws.extension.id, "builtin.multi_target.aws");
732
733 let terraform = resolve_builtin_backend_descriptor(BuiltinBackendId::Terraform)
734 .expect("terraform backend");
735 assert_eq!(terraform.backend_id, BuiltinBackendId::Terraform);
736 assert_eq!(
737 terraform.execution_kind,
738 BuiltinBackendExecutionKind::Executable
739 );
740 assert_eq!(terraform.handler_id, BuiltinBackendHandlerId::Terraform);
741 assert_eq!(terraform.extension.id, "builtin.multi_target.generic");
742 }
743
744 #[test]
745 fn builtin_extension_target_name_resolution_supports_single_vm_and_cloud_targets() {
746 let single_vm =
747 resolve_builtin_extension_for_target_name("single-vm").expect("single-vm target");
748 assert_eq!(single_vm.id, "builtin.single_vm.core");
749
750 let aws = resolve_builtin_extension_for_target_name("aws").expect("aws target");
751 assert_eq!(aws.id, "builtin.multi_target.aws");
752
753 assert!(resolve_builtin_extension_for_target_name("unknown").is_none());
754 }
755
756 #[test]
757 fn builtin_extension_detail_exposes_aliases_provider_and_backends() {
758 let aws = resolve_builtin_extension_detail_for_provider(Provider::Aws)
759 .expect("aws extension detail");
760 assert_eq!(aws.extension.id, "builtin.multi_target.aws");
761 assert_eq!(aws.provider, Some(Provider::Aws));
762 assert!(aws.aliases.iter().any(|alias| alias == "aws"));
763 assert_eq!(aws.backends.len(), 1);
764 assert_eq!(aws.backends[0].backend_id, BuiltinBackendId::Aws);
765 assert_eq!(
766 aws.backends[0].execution_kind,
767 BuiltinBackendExecutionKind::Cloud
768 );
769 assert_eq!(aws.backends[0].handler_id, BuiltinBackendHandlerId::Aws);
770 assert_eq!(
771 aws.backends[0].supported_capabilities,
772 STANDARD_DEPLOYER_CAPABILITIES
773 );
774 }
775
776 #[test]
777 fn list_builtin_extensions_returns_single_registry_view() {
778 let extensions = list_builtin_extensions();
779 assert!(
780 extensions
781 .iter()
782 .any(|detail| detail.extension.id == "builtin.multi_target.aws")
783 );
784 assert!(
785 extensions
786 .iter()
787 .any(|detail| detail.extension.id == "builtin.single_vm.core")
788 );
789 }
790
791 #[test]
792 fn builtin_handler_descriptor_exposes_extension_and_capabilities() {
793 let handler =
794 resolve_builtin_handler_descriptor(BuiltinBackendHandlerId::Aws).expect("aws handler");
795 assert_eq!(handler.backend_id, BuiltinBackendId::Aws);
796 assert_eq!(handler.extension.id, "builtin.multi_target.aws");
797 assert_eq!(
798 handler.supported_capabilities,
799 STANDARD_DEPLOYER_CAPABILITIES
800 );
801 }
802
803 #[test]
804 fn list_builtin_handlers_returns_registry_level_handler_view() {
805 let handlers = list_builtin_handlers();
806 assert!(
807 handlers
808 .iter()
809 .any(|handler| handler.handler_id == BuiltinBackendHandlerId::Terraform)
810 );
811 assert!(
812 handlers
813 .iter()
814 .any(|handler| handler.handler_id == BuiltinBackendHandlerId::Aws)
815 );
816 }
817
818 #[test]
819 fn deployment_extension_contract_exposes_generic_handler_contract() {
820 let aws = resolve_deployment_extension_contract_for_provider(Provider::Aws)
821 .expect("aws deployment extension contract");
822 assert_eq!(aws.extension.id, "builtin.multi_target.aws");
823 assert_eq!(aws.provider, Some(Provider::Aws));
824 assert!(
825 aws.handlers
826 .iter()
827 .any(|handler| handler.id == "builtin.aws")
828 );
829 assert!(
830 aws.handlers.iter().all(|handler| {
831 handler.supported_capabilities == STANDARD_DEPLOYER_CAPABILITIES
832 })
833 );
834 }
835
836 #[test]
837 fn list_deployment_extension_contracts_returns_generic_registry_view() {
838 let contracts = list_deployment_extension_contracts();
839 assert!(
840 contracts
841 .iter()
842 .any(|contract| contract.extension.id == "builtin.multi_target.aws")
843 );
844 assert!(
845 contracts
846 .iter()
847 .any(|contract| contract.extension.id == "builtin.single_vm.core")
848 );
849 }
850}
851
852#[cfg(test)]
853mod ext_roundtrip_tests {
854 use super::*;
855 use std::str::FromStr;
856
857 #[test]
858 fn from_str_all_variants_roundtrip() {
859 let cases = [
860 ("terraform", BuiltinBackendId::Terraform),
861 ("k8s_raw", BuiltinBackendId::K8sRaw),
862 ("helm", BuiltinBackendId::Helm),
863 ("aws", BuiltinBackendId::Aws),
864 ("azure", BuiltinBackendId::Azure),
865 ("gcp", BuiltinBackendId::Gcp),
866 ("juju_k8s", BuiltinBackendId::JujuK8s),
867 ("juju_machine", BuiltinBackendId::JujuMachine),
868 ("operator", BuiltinBackendId::Operator),
869 ("serverless", BuiltinBackendId::Serverless),
870 ("snap", BuiltinBackendId::Snap),
871 ];
872 for (s, expected) in cases {
873 assert_eq!(BuiltinBackendId::from_str(s).unwrap(), expected);
874 assert_eq!(expected.as_str(), s);
875 }
876 }
877
878 #[test]
879 fn from_str_rejects_unknown() {
880 let err = BuiltinBackendId::from_str("mystery").unwrap_err();
881 assert!(err.to_string().contains("mystery"));
882 }
883
884 #[test]
885 fn from_str_is_case_sensitive() {
886 assert!(BuiltinBackendId::from_str("AWS").is_err());
887 assert!(BuiltinBackendId::from_str("Terraform").is_err());
888 }
889
890 #[test]
891 fn handler_matches_permits_none_for_all() {
892 for b in [
893 BuiltinBackendId::Terraform,
894 BuiltinBackendId::Aws,
895 BuiltinBackendId::Helm,
896 ] {
897 assert!(b.handler_matches(None));
898 }
899 }
900
901 #[test]
902 fn handler_matches_rejects_unknown_for_all_existing() {
903 assert!(!BuiltinBackendId::Aws.handler_matches(Some("eks")));
904 }
905
906 #[test]
907 fn desktop_variant_roundtrip() {
908 use std::str::FromStr;
909 assert_eq!(
910 BuiltinBackendId::from_str("desktop").unwrap(),
911 BuiltinBackendId::Desktop
912 );
913 assert_eq!(BuiltinBackendId::Desktop.as_str(), "desktop");
914 }
915
916 #[test]
917 fn desktop_handler_matches_docker_compose_and_podman() {
918 assert!(BuiltinBackendId::Desktop.handler_matches(Some("docker-compose")));
919 assert!(BuiltinBackendId::Desktop.handler_matches(Some("podman")));
920 assert!(!BuiltinBackendId::Desktop.handler_matches(Some("kubernetes")));
921 assert!(BuiltinBackendId::Desktop.handler_matches(None));
922 }
923
924 #[test]
925 fn single_vm_variant_roundtrip() {
926 use std::str::FromStr;
927 assert_eq!(
928 BuiltinBackendId::from_str("single_vm").unwrap(),
929 BuiltinBackendId::SingleVm
930 );
931 assert_eq!(BuiltinBackendId::SingleVm.as_str(), "single_vm");
932 }
933
934 #[test]
935 fn single_vm_handler_matches_rejects_any_handler() {
936 assert!(BuiltinBackendId::SingleVm.handler_matches(None));
937 assert!(!BuiltinBackendId::SingleVm.handler_matches(Some("docker")));
938 assert!(!BuiltinBackendId::SingleVm.handler_matches(Some("foo")));
939 }
940}