1use std::path::Path;
14
15use crate::config::{PodmanWorkspaceStorage, SshConnection, TargetTemplate};
16use crate::state::{PodmanWorkspaceLocator, TargetLocator};
17use crate::targets;
18
19fn path_text(path: &Path) -> String {
21 path.to_string_lossy().into_owned()
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum TargetConversionError {
31 KindMismatch {
33 locator: &'static str,
34 template: &'static str,
35 },
36 SshHostMismatch { locator: String, template: String },
38 MissingAwsAddress,
40}
41
42impl std::fmt::Display for TargetConversionError {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::KindMismatch { locator, template } => write!(
46 formatter,
47 "session locator/template mismatch: locator is {locator}, template is {template}"
48 ),
49 Self::SshHostMismatch { locator, template } => write!(
50 formatter,
51 "session locator/template SSH host mismatch: locator is {locator:?}, template is {template:?}"
52 ),
53 Self::MissingAwsAddress => formatter.write_str("AWS locator has no address"),
54 }
55 }
56}
57
58impl std::error::Error for TargetConversionError {}
59
60pub fn ssh_args_with_identity(args: &[String], identity: Option<&Path>) -> Vec<String> {
68 let mut result = vec![
69 "-o".into(),
70 "BatchMode=yes".into(),
71 "-o".into(),
72 "StrictHostKeyChecking=accept-new".into(),
73 "-o".into(),
74 "ConnectTimeout=15".into(),
75 ];
76 result.extend(args.iter().cloned());
77 if let Some(identity) = identity {
78 result.push("-i".into());
79 result.push(path_text(identity));
80 }
81 result
82}
83
84impl From<&SshConnection> for targets::SshTarget {
85 fn from(ssh: &SshConnection) -> Self {
86 let destination = match &ssh.user {
87 Some(user) => format!("{user}@{}", ssh.host),
88 None => ssh.host.clone(),
89 };
90 Self {
91 destination,
92 ssh_args: ssh_args_with_identity(&ssh.extra_args, ssh.identity_file.as_deref()),
93 }
94 }
95}
96
97impl From<&PodmanWorkspaceStorage> for targets::PodmanWorkspaceStorage {
98 fn from(storage: &PodmanWorkspaceStorage) -> Self {
99 match storage {
100 PodmanWorkspaceStorage::PodmanVolume => Self::PodmanVolume,
101 PodmanWorkspaceStorage::HostHelper { root, helper } => Self::HostHelper {
102 root: path_text(root),
103 helper: helper.clone(),
104 },
105 PodmanWorkspaceStorage::ContainerLayer => Self::ContainerLayer,
106 }
107 }
108}
109
110impl From<&PodmanWorkspaceLocator> for targets::PodmanWorkspaceLocator {
111 fn from(storage: &PodmanWorkspaceLocator) -> Self {
112 match storage {
113 PodmanWorkspaceLocator::ContainerLayer => Self::ContainerLayer,
114 PodmanWorkspaceLocator::Volume { name } => Self::Volume { name: name.clone() },
115 PodmanWorkspaceLocator::HostPath {
116 path,
117 helper,
118 resource,
119 } => Self::HostPath {
120 path: path_text(path),
121 helper: helper.clone(),
122 resource: resource.clone(),
123 },
124 }
125 }
126}
127
128impl From<targets::PodmanWorkspaceLocator> for PodmanWorkspaceLocator {
129 fn from(storage: targets::PodmanWorkspaceLocator) -> Self {
130 match storage {
131 targets::PodmanWorkspaceLocator::ContainerLayer => Self::ContainerLayer,
132 targets::PodmanWorkspaceLocator::Volume { name } => Self::Volume { name },
133 targets::PodmanWorkspaceLocator::HostPath {
134 path,
135 helper,
136 resource,
137 } => Self::HostPath {
138 path: std::path::PathBuf::from(path),
139 helper,
140 resource,
141 },
142 }
143 }
144}
145
146#[derive(Debug, Clone, Copy)]
153pub struct StoredTarget<'a> {
154 pub locator: &'a TargetLocator,
155 pub template: &'a TargetTemplate,
156 pub session_id: &'a str,
157}
158
159impl TryFrom<StoredTarget<'_>> for targets::TargetLocator {
160 type Error = TargetConversionError;
161
162 fn try_from(stored: StoredTarget<'_>) -> Result<Self, Self::Error> {
163 let StoredTarget {
164 locator,
165 template,
166 session_id,
167 } = stored;
168 let mismatch = || TargetConversionError::KindMismatch {
169 locator: locator_kind_name(locator),
170 template: template.kind_name(),
171 };
172 Ok(match locator {
173 TargetLocator::LocalBare { worker_root } => {
174 let TargetTemplate::LocalBare = template else {
175 return Err(mismatch());
176 };
177 Self::LocalBare {
178 worker_root: path_text(worker_root),
179 }
180 }
181 TargetLocator::LocalPodman {
182 container_id,
183 workspace_storage,
184 borrowed_from,
185 } => Self::LocalPodman {
186 borrowed_from: borrowed_from.clone(),
187 container_id: container_id.clone(),
188 workspace_storage: workspace_storage.into(),
189 },
190 TargetLocator::LocalDocker {
191 container_id,
192 borrowed_from,
193 } => Self::LocalDocker {
194 borrowed_from: borrowed_from.clone(),
195 container_id: container_id.clone(),
196 },
197 TargetLocator::AppleContainer {
198 container_id,
199 borrowed_from,
200 } => Self::AppleContainer {
201 borrowed_from: borrowed_from.clone(),
202 container_id: container_id.clone(),
203 },
204 TargetLocator::SshBare {
205 workspace,
206 worker_id,
207 ..
208 } => {
209 let TargetTemplate::SshBare { ssh, .. } = template else {
210 return Err(mismatch());
211 };
212 Self::SshBare {
213 ssh: ssh.into(),
214 workspace: path_text(workspace),
215 worker_id: worker_id.clone(),
216 }
217 }
218 TargetLocator::SshPodman {
219 container_id,
220 workspace_storage,
221 borrowed_from,
222 ..
223 } => {
224 let TargetTemplate::SshPodman { ssh, .. } = template else {
225 return Err(mismatch());
226 };
227 Self::SshPodman {
228 borrowed_from: borrowed_from.clone(),
229 ssh: ssh.into(),
230 container_id: container_id.clone(),
231 workspace_storage: workspace_storage.into(),
232 }
233 }
234 TargetLocator::SshDocker {
235 host,
236 container_id,
237 borrowed_from,
238 } => {
239 let TargetTemplate::SshDocker { ssh, .. } = template else {
240 return Err(mismatch());
241 };
242 if host != &ssh.host {
243 return Err(TargetConversionError::SshHostMismatch {
244 locator: host.clone(),
245 template: ssh.host.clone(),
246 });
247 }
248 Self::SshDocker {
249 borrowed_from: borrowed_from.clone(),
250 ssh: ssh.into(),
251 container_id: container_id.clone(),
252 }
253 }
254 TargetLocator::AwsEc2 {
255 instance_id,
256 address,
257 } => {
258 let TargetTemplate::AwsEc2 {
259 aws_profile,
260 region,
261 ssh_user,
262 identity_file,
263 ssh_args,
264 ..
265 } = template
266 else {
267 return Err(mismatch());
268 };
269 let address = address
270 .as_deref()
271 .ok_or(TargetConversionError::MissingAwsAddress)?;
272 Self::AwsEc2 {
273 profile: aws_profile.clone().unwrap_or_else(|| "default".into()),
274 region: region.clone(),
275 instance_id: instance_id.clone(),
276 ssh: targets::SshTarget {
277 destination: format!("{ssh_user}@{address}"),
278 ssh_args: ssh_args_with_identity(ssh_args, identity_file.as_deref()),
279 },
280 workspace: targets::aws_workspace(session_id),
281 }
282 }
283 })
284 }
285}
286
287const fn locator_kind_name(locator: &TargetLocator) -> &'static str {
290 match locator {
291 TargetLocator::LocalBare { .. } => "local-bare",
292 TargetLocator::LocalPodman { .. } => "local-podman",
293 TargetLocator::LocalDocker { .. } => "local-docker",
294 TargetLocator::AppleContainer { .. } => "apple-container",
295 TargetLocator::AwsEc2 { .. } => "aws-ec2",
296 TargetLocator::SshBare { .. } => "ssh-bare",
297 TargetLocator::SshPodman { .. } => "ssh-podman",
298 TargetLocator::SshDocker { .. } => "ssh-docker",
299 }
300}