Skip to main content

xianyun_cluster/driver/
external.rs

1//! Port of `internal/cluster/external/driver.go`: a [`super::Driver`]
2//! implementation that adopts an already-running Kubernetes cluster through
3//! an existing kubeconfig context, rather than provisioning one.
4//!
5//! **Executor injection.** The Go original reaches its `CommandRunner`
6//! through an ambient `context.Context` (`executor.FromContext(ctx)`)
7//! threaded through every method call. Every [`super::Driver`] method in
8//! this crate has no context parameter at all (see `mock.rs`), so rather
9//! than route through `xianyun_executor::exec::context` (a sibling crate's
10//! concern, and a different shape than this trait's method signatures),
11//! this driver takes its `Arc<dyn CommandRunner + Send + Sync>` once, at
12//! construction, and stores it as a field. Tests inject
13//! `xianyun_testutil::MockCommandRunner` the same way.
14
15use std::sync::Arc;
16
17use super::{Driver as DriverTrait, DriverError, State, Status};
18use xianyun_core::config::types::ClusterConfig;
19use xianyun_executor::exec::CommandRunner;
20
21pub const DRIVER_NAME: &str = "external";
22
23/// Port of `external.ErrClusterUnreachable`, `ErrCannotStopExternal`, and
24/// `ErrCannotDestroyExternal`.
25#[derive(Debug, thiserror::Error, PartialEq, Eq)]
26pub enum ExternalError {
27    #[error("cannot reach the external cluster")]
28    ClusterUnreachable,
29    #[error("cannot stop an external cluster: caproni does not manage its lifecycle")]
30    CannotStopExternal,
31    #[error("cannot destroy an external cluster: caproni does not manage its lifecycle")]
32    CannotDestroyExternal,
33}
34
35/// Port of `external.Driver`.
36pub struct Driver {
37    pub cfg: ClusterConfig,
38    pub context_name: String,
39    runner: Arc<dyn CommandRunner + Send + Sync>,
40}
41
42impl Driver {
43    /// Port of `external.NewDriver`. Takes the injected `CommandRunner`
44    /// explicitly; see the module doc comment for why.
45    #[must_use]
46    pub fn new(cfg: ClusterConfig, runner: Arc<dyn CommandRunner + Send + Sync>) -> Self {
47        let context_name = cfg
48            .external
49            .as_ref()
50            .map(|e| e.context.clone())
51            .unwrap_or_default();
52
53        Self {
54            cfg,
55            context_name,
56            runner,
57        }
58    }
59
60    /// Port of `(*external.Driver).contextArgs`.
61    fn context_args(&self) -> Vec<String> {
62        if self.context_name.is_empty() {
63            Vec::new()
64        } else {
65            vec!["--context".to_string(), self.context_name.clone()]
66        }
67    }
68
69    /// Port of `(*external.Driver).kubectlArgs`.
70    fn kubectl_args(&self, args: &[&str]) -> Vec<String> {
71        let mut out = self.context_args();
72        out.extend(args.iter().map(std::string::ToString::to_string));
73        out
74    }
75
76    /// Port of `(*external.Driver).ensureKubectl`.
77    fn ensure_kubectl(&self) -> Result<(), DriverError> {
78        self.runner
79            .look_path("kubectl")
80            .map(|_| ())
81            .map_err(|_e| DriverError::DriverNotFound("kubectl".to_string()))
82    }
83
84    /// Port of `(*external.Driver).contextResolvedName`.
85    fn context_resolved_name(&self) -> &str {
86        if self.context_name.is_empty() {
87            "(current)"
88        } else {
89            &self.context_name
90        }
91    }
92}
93
94impl DriverTrait for Driver {
95    fn name(&self) -> &str {
96        DRIVER_NAME
97    }
98
99    fn start(&self) -> Result<(), DriverError> {
100        if self.cfg.registry_mirror.is_some() {
101            tracing::warn!(
102                "registry_mirror is currently unsupported for the external driver and will be ignored"
103            );
104        }
105
106        self.ensure_kubectl()?;
107
108        let args = self.kubectl_args(&["cluster-info"]);
109        let out = self.runner.read_stdout("kubectl", &args).map_err(|e| {
110            DriverError::Other(format!("{}: {e}", ExternalError::ClusterUnreachable))
111        })?;
112
113        tracing::info!(
114            context = self.context_resolved_name(),
115            "cluster-info" = first_line(&out),
116            "connected to external cluster"
117        );
118
119        Ok(())
120    }
121
122    fn stop(&self) -> Result<(), DriverError> {
123        Err(DriverError::Other(
124            ExternalError::CannotStopExternal.to_string(),
125        ))
126    }
127
128    fn destroy(&self) -> Result<(), DriverError> {
129        Err(DriverError::Other(
130            ExternalError::CannotDestroyExternal.to_string(),
131        ))
132    }
133
134    fn status(&self) -> Result<Status, DriverError> {
135        self.ensure_kubectl()?;
136
137        let mut status = Status {
138            driver: DRIVER_NAME.to_string(),
139            state: Some(State::Stopped),
140            kube_context: self.context_name.clone(),
141            ..Default::default()
142        };
143
144        let args = self.kubectl_args(&["version", "-o", "json", "--request-timeout=5s"]);
145        // Unreachable is a valid state, not an error.
146        let Ok(out) = self.runner.read_stdout("kubectl", &args) else {
147            return Ok(status);
148        };
149
150        let version: KubectlVersion = serde_json::from_str(&out)
151            .map_err(|e| DriverError::Other(format!("parsing kubectl version JSON: {e}")))?;
152
153        if let Some(server_version) = version.server_version
154            && !server_version.git_version.is_empty()
155        {
156            status.state = Some(State::Ready);
157            status.exists = true;
158            status.kubernetes_version = server_version.git_version;
159        }
160
161        Ok(status)
162    }
163
164    fn get_kubeconfig(&self) -> Result<String, DriverError> {
165        self.ensure_kubectl()?;
166
167        let args = self.kubectl_args(&["config", "view", "--raw", "--minify", "--flatten"]);
168        self.runner
169            .read_stdout("kubectl", &args)
170            .map_err(|e| DriverError::Other(format!("extracting kubeconfig: {e}")))
171    }
172}
173
174/// Port of `external.kubectlVersion`, the JSON shape of `kubectl version -o json`.
175#[derive(Debug, Clone, Default, serde::Deserialize)]
176pub struct KubectlVersion {
177    #[serde(rename = "serverVersion")]
178    pub server_version: Option<ServerVersion>,
179}
180
181/// Port of the nested `serverVersion` object in `kubectlVersion`.
182#[derive(Debug, Clone, Default, serde::Deserialize)]
183pub struct ServerVersion {
184    #[serde(rename = "gitVersion", default)]
185    pub git_version: String,
186}
187
188/// Port of `external.firstLine`.
189#[must_use]
190pub fn first_line(s: &str) -> &str {
191    match s.find('\n') {
192        Some(idx) => &s[..idx],
193        None => s,
194    }
195}