xianyun_cluster/driver/
external.rs1use 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#[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
35pub struct Driver {
37 pub cfg: ClusterConfig,
38 pub context_name: String,
39 runner: Arc<dyn CommandRunner + Send + Sync>,
40}
41
42impl Driver {
43 #[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 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 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 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 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 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#[derive(Debug, Clone, Default, serde::Deserialize)]
176pub struct KubectlVersion {
177 #[serde(rename = "serverVersion")]
178 pub server_version: Option<ServerVersion>,
179}
180
181#[derive(Debug, Clone, Default, serde::Deserialize)]
183pub struct ServerVersion {
184 #[serde(rename = "gitVersion", default)]
185 pub git_version: String,
186}
187
188#[must_use]
190pub fn first_line(s: &str) -> &str {
191 match s.find('\n') {
192 Some(idx) => &s[..idx],
193 None => s,
194 }
195}