Skip to main content

greentic_deployer/
extension.rs

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    /// Return `true` iff this backend accepts the given handler string.
50    /// Desktop accepts `None`, `"docker-compose"`, or `"podman"`.
51    /// All other backends have a single implicit handler; `None` matches
52    /// and any explicit value is rejected.
53    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}