1use std::path::Path;
14
15use crate::config::{ContainerTemplate, 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 TargetTemplate {
98 pub fn image_host(&self) -> Option<(targets::ImageHost, &ContainerTemplate)> {
106 match self {
107 Self::LocalPodman { container } => Some((targets::ImageHost::LocalPodman, container)),
108 Self::LocalDocker { container } => Some((targets::ImageHost::LocalDocker, container)),
109 Self::AppleContainer { container } => {
110 Some((targets::ImageHost::AppleContainer, container))
111 }
112 Self::SshPodman { ssh, container } => Some((
113 targets::ImageHost::SshPodman(targets::SshTarget::from(ssh)),
114 container,
115 )),
116 Self::SshDocker { ssh, container } => Some((
117 targets::ImageHost::SshDocker(targets::SshTarget::from(ssh)),
118 container,
119 )),
120 Self::LocalBare | Self::AwsEc2 { .. } | Self::SshBare { .. } => None,
121 }
122 }
123}
124
125impl From<&PodmanWorkspaceStorage> for targets::PodmanWorkspaceStorage {
126 fn from(storage: &PodmanWorkspaceStorage) -> Self {
127 match storage {
128 PodmanWorkspaceStorage::PodmanVolume => Self::PodmanVolume,
129 PodmanWorkspaceStorage::HostHelper { root, helper } => Self::HostHelper {
130 root: path_text(root),
131 helper: helper.clone(),
132 },
133 PodmanWorkspaceStorage::ContainerLayer => Self::ContainerLayer,
134 }
135 }
136}
137
138impl From<&PodmanWorkspaceLocator> for targets::PodmanWorkspaceLocator {
139 fn from(storage: &PodmanWorkspaceLocator) -> Self {
140 match storage {
141 PodmanWorkspaceLocator::ContainerLayer => Self::ContainerLayer,
142 PodmanWorkspaceLocator::Volume { name } => Self::Volume { name: name.clone() },
143 PodmanWorkspaceLocator::HostPath {
144 path,
145 helper,
146 resource,
147 } => Self::HostPath {
148 path: path_text(path),
149 helper: helper.clone(),
150 resource: resource.clone(),
151 },
152 }
153 }
154}
155
156impl From<targets::PodmanWorkspaceLocator> for PodmanWorkspaceLocator {
157 fn from(storage: targets::PodmanWorkspaceLocator) -> Self {
158 match storage {
159 targets::PodmanWorkspaceLocator::ContainerLayer => Self::ContainerLayer,
160 targets::PodmanWorkspaceLocator::Volume { name } => Self::Volume { name },
161 targets::PodmanWorkspaceLocator::HostPath {
162 path,
163 helper,
164 resource,
165 } => Self::HostPath {
166 path: std::path::PathBuf::from(path),
167 helper,
168 resource,
169 },
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy)]
181pub struct StoredTarget<'a> {
182 pub locator: &'a TargetLocator,
183 pub template: &'a TargetTemplate,
184 pub session_id: &'a str,
185}
186
187impl TryFrom<StoredTarget<'_>> for targets::TargetLocator {
188 type Error = TargetConversionError;
189
190 fn try_from(stored: StoredTarget<'_>) -> Result<Self, Self::Error> {
191 let StoredTarget {
192 locator,
193 template,
194 session_id,
195 } = stored;
196 let mismatch = || TargetConversionError::KindMismatch {
197 locator: locator_kind_name(locator),
198 template: template.kind_name(),
199 };
200 Ok(match locator {
201 TargetLocator::LocalBare { worker_root } => {
202 let TargetTemplate::LocalBare = template else {
203 return Err(mismatch());
204 };
205 Self::LocalBare {
206 worker_root: path_text(worker_root),
207 }
208 }
209 TargetLocator::LocalPodman {
210 container_id,
211 workspace_storage,
212 borrowed_from,
213 } => Self::LocalPodman {
214 borrowed_from: borrowed_from.clone(),
215 container_id: container_id.clone(),
216 workspace_storage: workspace_storage.into(),
217 },
218 TargetLocator::LocalDocker {
219 container_id,
220 borrowed_from,
221 } => Self::LocalDocker {
222 borrowed_from: borrowed_from.clone(),
223 container_id: container_id.clone(),
224 },
225 TargetLocator::AppleContainer {
226 container_id,
227 borrowed_from,
228 } => Self::AppleContainer {
229 borrowed_from: borrowed_from.clone(),
230 container_id: container_id.clone(),
231 },
232 TargetLocator::SshBare {
233 workspace,
234 worker_id,
235 ..
236 } => {
237 let TargetTemplate::SshBare { ssh, .. } = template else {
238 return Err(mismatch());
239 };
240 Self::SshBare {
241 ssh: ssh.into(),
242 workspace: path_text(workspace),
243 worker_id: worker_id.clone(),
244 }
245 }
246 TargetLocator::SshPodman {
247 container_id,
248 workspace_storage,
249 borrowed_from,
250 ..
251 } => {
252 let TargetTemplate::SshPodman { ssh, .. } = template else {
253 return Err(mismatch());
254 };
255 Self::SshPodman {
256 borrowed_from: borrowed_from.clone(),
257 ssh: ssh.into(),
258 container_id: container_id.clone(),
259 workspace_storage: workspace_storage.into(),
260 }
261 }
262 TargetLocator::SshDocker {
263 host,
264 container_id,
265 borrowed_from,
266 } => {
267 let TargetTemplate::SshDocker { ssh, .. } = template else {
268 return Err(mismatch());
269 };
270 if host != &ssh.host {
271 return Err(TargetConversionError::SshHostMismatch {
272 locator: host.clone(),
273 template: ssh.host.clone(),
274 });
275 }
276 Self::SshDocker {
277 borrowed_from: borrowed_from.clone(),
278 ssh: ssh.into(),
279 container_id: container_id.clone(),
280 }
281 }
282 TargetLocator::AwsEc2 {
283 instance_id,
284 address,
285 } => {
286 let TargetTemplate::AwsEc2 {
287 aws_profile,
288 region,
289 ssh_user,
290 identity_file,
291 ssh_args,
292 ..
293 } = template
294 else {
295 return Err(mismatch());
296 };
297 let address = address
298 .as_deref()
299 .ok_or(TargetConversionError::MissingAwsAddress)?;
300 Self::AwsEc2 {
301 profile: aws_profile.clone().unwrap_or_else(|| "default".into()),
302 region: region.clone(),
303 instance_id: instance_id.clone(),
304 ssh: targets::SshTarget {
305 destination: format!("{ssh_user}@{address}"),
306 ssh_args: ssh_args_with_identity(ssh_args, identity_file.as_deref()),
307 },
308 workspace: targets::aws_workspace(session_id),
309 }
310 }
311 })
312 }
313}
314
315const fn locator_kind_name(locator: &TargetLocator) -> &'static str {
318 match locator {
319 TargetLocator::LocalBare { .. } => "local-bare",
320 TargetLocator::LocalPodman { .. } => "local-podman",
321 TargetLocator::LocalDocker { .. } => "local-docker",
322 TargetLocator::AppleContainer { .. } => "apple-container",
323 TargetLocator::AwsEc2 { .. } => "aws-ec2",
324 TargetLocator::SshBare { .. } => "ssh-bare",
325 TargetLocator::SshPodman { .. } => "ssh-podman",
326 TargetLocator::SshDocker { .. } => "ssh-docker",
327 }
328}