1pub mod deployment;
4
5use cfg_if::cfg_if;
6use color_eyre::eyre::Context;
7
8use crate::profile::{source::PunktfSource, MergeMode};
9use crate::visit::*;
10
11use crate::profile::transform::Transform as _;
12use crate::profile::LayeredProfile;
13use crate::visit::deploy::deployment::{Deployment, DeploymentBuilder, ItemStatus};
14use std::borrow::Borrow;
15use std::path::Path;
16
17use crate::visit::{ResolvingVisitor, TemplateVisitor};
18
19enum SafeRead {
21 String(String),
23
24 Binary(Vec<u8>),
26}
27
28fn safe_read<P: AsRef<Path>>(path: P) -> io::Result<SafeRead> {
31 fn inner(path: &Path) -> io::Result<SafeRead> {
33 match std::fs::read_to_string(path) {
34 Ok(s) => Ok(SafeRead::String(s)),
35 Err(err) if err.kind() == io::ErrorKind::InvalidData => {
36 std::fs::read(path).map(SafeRead::Binary)
37 }
38 Err(err) => Err(err),
39 }
40 }
41
42 inner(path.as_ref())
43}
44
45impl<'a> Item<'a> {
46 fn add_to_builder<S: Into<ItemStatus>>(&self, builder: &mut DeploymentBuilder, status: S) {
49 let status = status.into();
50
51 let resolved_target_path = self
52 .target_path
53 .canonicalize()
54 .unwrap_or_else(|_| self.target_path.clone());
55
56 match &self.kind {
57 Kind::Root(dotfile) => {
58 builder.add_dotfile(resolved_target_path, (*dotfile).clone(), status)
59 }
60 Kind::Child {
61 root_target_path, ..
62 } => {
63 let resolved_root_target_path = root_target_path
64 .canonicalize()
65 .unwrap_or_else(|_| root_target_path.clone());
66
67 builder.add_child(resolved_target_path, resolved_root_target_path, status)
68 }
69 };
70 }
71}
72
73impl Symlink {
74 fn add_to_builder<S: Into<ItemStatus>>(&self, builder: &mut DeploymentBuilder, status: S) {
77 builder.add_link(
78 self.source_path.clone(),
79 self.target_path.clone(),
80 status.into(),
81 );
82 }
83}
84
85macro_rules! success {
87 ($builder:expr, $item:expr) => {
88 $item.add_to_builder($builder, ItemStatus::success());
89 };
90}
91
92macro_rules! skipped {
96 ($builder:expr, $item:expr, $reason:expr => $ret:expr ) => {
97 $item.add_to_builder($builder, ItemStatus::skipped($reason));
98 return Ok($ret);
99 };
100 ($builder:expr, $item:expr, $reason:expr) => {
101 $item.add_to_builder($builder, ItemStatus::skipped($reason));
102 return Ok(());
103 };
104}
105
106macro_rules! failed {
110 ($builder:expr, $item:expr, $reason:expr => Err($ret:expr) ) => {
111 $item.add_to_builder($builder, ItemStatus::failed($reason));
112 return Err($ret);
113 };
114 ($builder:expr, $item:expr, $reason:expr => $ret:expr ) => {
115 $item.add_to_builder($builder, ItemStatus::failed($reason));
116 return Ok($ret);
117 };
118 ($builder:expr, $item:expr, $reason:expr) => {
119 $item.add_to_builder($builder, ItemStatus::failed($reason));
120 return Ok(());
121 };
122}
123
124#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub struct DeployOptions {
127 pub dry_run: bool,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct Deployer<F> {
141 options: DeployOptions,
143
144 merge_ask_fn: F,
150
151 builder: DeploymentBuilder,
156}
157
158impl<F> Deployer<F>
159where
160 F: Fn(&Path, &Path) -> color_eyre::Result<bool>,
161{
162 pub fn new(options: DeployOptions, merge_ask_fn: F) -> Self {
164 Self {
165 options,
166 merge_ask_fn,
167 builder: DeploymentBuilder::default(),
168 }
169 }
170
171 pub fn into_deployment(self) -> Deployment {
173 self.builder.finish()
174 }
175
176 pub fn deploy(self, source: &PunktfSource, profile: &mut LayeredProfile) -> Deployment {
184 for hook in profile.pre_hooks() {
203 log::info!("Executing pre-hook: {}", hook.command());
204 if let Err(err) = hook
209 .execute(source.profiles())
210 .wrap_err("Failed to execute pre-hook")
211 {
212 log::error!("Failed to execute pre-hook ({})", err);
213 return self.builder.failed(err.to_string());
214 };
215 }
216
217 let mut resolver = ResolvingVisitor(self);
218 let walker = Walker::new(profile);
219 if let Err(err) = walker.walk(source, &mut resolver) {
220 return resolver.into_inner().builder.failed(err.to_string());
221 }
222
223 let this = resolver.into_inner();
224
225 for hook in profile.post_hooks() {
226 log::info!("Executing post-hook: {}", hook.command());
227 if let Err(err) = hook.execute(source.profiles()) {
228 log::error!("Failed to execute post-hook ({})", err);
229 return this.builder.failed(err.to_string());
230 }
231 }
232
233 this.into_deployment()
234 }
235
236 fn pre_deploy_checks(&mut self, file: &File<'_>) -> color_eyre::Result<bool> {
241 let other_priority = self.builder.get_priority(&file.target_path);
242
243 match (file.dotfile().priority.as_ref(), other_priority) {
244 (Some(a), Some(b)) if b > a => {
245 log::info!(
246 "[{}] Dotfile with higher priority is already deployed at {}",
247 file.relative_source_path.display(),
248 file.target_path.display()
249 );
250
251 skipped!(&mut self.builder, file, "Dotfile with higher priority is already deployed" => false);
252 }
253 (_, _) => {}
254 };
255
256 if file.target_path.exists() {
257 log::debug!(
260 "[{}] Dotfile already exists at {}",
261 file.relative_source_path.display(),
262 file.target_path.display()
263 );
264
265 match file.dotfile().merge.unwrap_or_default() {
266 MergeMode::Overwrite => {
267 log::info!(
268 "[{}] Overwriting existing dotfile",
269 file.relative_source_path.display()
270 )
271 }
272 MergeMode::Keep => {
273 log::info!(
274 "[{}] Skipping existing dotfile",
275 file.relative_source_path.display()
276 );
277
278 skipped!(&mut self.builder, file, format!("Dotfile already exists and merge mode is {:?}", MergeMode::Keep) => false);
279 }
280 MergeMode::Ask => {
281 log::info!(
282 "[{}] Asking for action",
283 file.relative_source_path.display()
284 );
285
286 let should_deploy = match (self.merge_ask_fn)(
287 &file.source_path,
288 file.target_path.borrow(),
289 )
290 .wrap_err("Error evaluating user response")
291 {
292 Ok(should_deploy) => should_deploy,
293 Err(err) => {
294 log::error!(
295 "[{}] Failed to execute ask function ({})",
296 file.relative_source_path.display(),
297 err
298 );
299
300 failed!(&mut self.builder, file, format!("Failed to execute merge ask function: {err}") => false);
301 }
302 };
303
304 if !should_deploy {
305 log::info!("{} Merge was denied", file.relative_source_path.display());
306
307 skipped!(&mut self.builder, file, "Dotfile already exists and merge ask was denied" => false);
308 }
309 }
310 }
311 }
312
313 if let Some(parent) = file.target_path.parent() {
314 if !self.options.dry_run {
315 match std::fs::create_dir_all(parent) {
316 Ok(_) => {}
317 Err(err) => {
318 log::error!(
319 "[{}] Failed to create directory ({})",
320 file.relative_source_path.display(),
321 err
322 );
323
324 failed!(&mut self.builder, file, format!("Failed to create parent directory: {err}") => false);
325 }
326 }
327 }
328 }
329
330 Ok(true)
331 }
332
333 fn transform_content(
336 &mut self,
337 profile: &LayeredProfile,
338 file: &File<'_>,
339 content: String,
340 ) -> color_eyre::Result<String> {
341 let mut content = content;
342
343 let exec_transformers: Vec<_> = file.dotfile().transformers.to_vec();
345
346 for transformer in profile.transformers().chain(exec_transformers.iter()) {
351 content = match transformer.transform(content) {
352 Ok(content) => content,
353 Err(err) => {
354 log::info!(
355 "[{}] Failed to apply content transformer `{}`: `{}`",
356 file.relative_source_path.display(),
357 transformer,
358 err
359 );
360
361 failed!(&mut self.builder, file, format!("Failed to apply content transformer `{transformer}`: `{err}`") => Err(err));
362 }
363 };
364 }
365
366 Ok(content)
367 }
368}
369
370impl<F> Visitor for Deployer<F>
371where
372 F: Fn(&Path, &Path) -> color_eyre::Result<bool>,
373{
374 fn accept_file<'a>(
376 &mut self,
377 _: &PunktfSource,
378 profile: &LayeredProfile,
379 file: &File<'a>,
380 ) -> Result {
381 log::info!("[{}] Deploying file", file.relative_source_path.display());
382
383 let cont = self.pre_deploy_checks(file)?;
384
385 if !cont {
386 return Ok(());
387 }
388
389 if profile.transformers_len() == 0 && file.dotfile().transformers.is_empty() {
391 #[allow(clippy::collapsible_else_if)]
396 if !self.options.dry_run {
397 if let Err(err) = std::fs::copy(&file.source_path, &file.target_path) {
398 log::info!(
399 "[{}] Failed to copy file",
400 file.relative_source_path.display()
401 );
402
403 failed!(&mut self.builder, file, format!("Failed to copy: {err}"));
404 }
405 }
406 } else {
407 let content = match safe_read(&file.source_path) {
408 Ok(SafeRead::Binary(b)) => {
409 log::info!(
410 "[{}] Not evaluated as template - Binary data",
411 file.relative_source_path.display()
412 );
413
414 b
415 }
416 Ok(SafeRead::String(s)) => {
417 let Ok(content) = self.transform_content(profile, file, s) else {
418 return Ok(());
420 };
421
422 content.into_bytes()
423 }
424 Err(err) => {
425 log::info!(
426 "[{}] Failed to read file",
427 file.relative_source_path.display()
428 );
429
430 failed!(&mut self.builder, file, format!("Failed to read: {err}"));
431 }
432 };
433
434 if !self.options.dry_run {
435 if let Err(err) = std::fs::write(&file.target_path, content) {
436 log::info!(
437 "[{}] Failed to write content",
438 file.relative_source_path.display()
439 );
440
441 failed!(
442 &mut self.builder,
443 file,
444 format!("Failed to write content: {err}")
445 );
446 }
447 }
448 }
449
450 log::info!(
451 "[{}] File successfully deployed",
452 file.relative_source_path.display()
453 );
454
455 success!(&mut self.builder, file);
456
457 Ok(())
458 }
459
460 fn accept_directory<'a>(
462 &mut self,
463 _: &PunktfSource,
464 _: &LayeredProfile,
465 directory: &Directory<'a>,
466 ) -> Result {
467 log::info!(
468 "[{}] Deploying directory",
469 directory.relative_source_path.display()
470 );
471
472 if !self.options.dry_run {
473 if let Err(err) = std::fs::create_dir_all(&directory.target_path) {
474 log::error!(
475 "[{}] Failed to create directory ({})",
476 directory.relative_source_path.display(),
477 err
478 );
479
480 failed!(
481 &mut self.builder,
482 directory,
483 format!("Failed to create directory: {err}")
484 );
485 } else {
486 success!(&mut self.builder, directory);
487 }
488 } else {
489 success!(&mut self.builder, directory);
490 }
491
492 log::info!(
493 "[{}] Directory successfully deployed",
494 directory.relative_source_path.display()
495 );
496
497 Ok(())
498 }
499
500 fn accept_link(&mut self, _: &PunktfSource, _: &LayeredProfile, link: &Symlink) -> Result {
502 log::info!("[{}] Deploying symlink", link.source_path.display());
503
504 #[cfg(all(not(unix), not(windows)))]
507 {
508 log::warn!(
509 "[{}] Symlink operations are only supported for unix and windows systems",
510 source_path.display()
511 );
512 skipped!(
513 &mut self.builder,
514 link,
515 "Symlink operations are only supported on unix and windows systems"
516 );
517 }
518
519 let source_path = &link.source_path;
520 let target_path = &link.target_path;
521
522 if !source_path.exists() {
524 log::error!("[{}] Links source does not exist", source_path.display());
525
526 failed!(&mut self.builder, link, "Link source does not exist");
527 }
528
529 if target_path.exists() {
531 if link.replace {
532 if !self.options.dry_run {
533 let target_metadata = match target_path.symlink_metadata() {
535 Ok(m) => m,
536 Err(err) => {
537 log::error!("[{}] Failed to read metadata", source_path.display());
538
539 failed!(
540 &mut self.builder,
541 link,
542 format!("Failed get link target metadata: {err}")
543 );
544 }
545 };
546
547 if target_metadata.is_symlink() {
548 let res = if let Ok(target_metadata) = target_path.metadata() {
550 if target_metadata.is_dir() {
551 std::fs::remove_dir(target_path)
552 } else {
553 std::fs::remove_file(target_path)
554 }
555 } else {
556 std::fs::remove_file(target_path)
557 .or_else(|_| std::fs::remove_dir(target_path))
558 };
559
560 if let Err(err) = res {
561 log::error!(
562 "[{}] Failed to remove old link at target",
563 source_path.display()
564 );
565
566 failed!(
567 &mut self.builder,
568 link,
569 format!("Failed to remove old link target: {err}")
570 );
571 } else {
572 log::info!(
573 "[{}] Removed old link target at {}",
574 source_path.display(),
575 target_path.display()
576 );
577 }
578 } else {
579 log::error!(
580 "[{}] Target already exists and is no link",
581 source_path.display()
582 );
583
584 failed!(&mut self.builder, link, "Not allowed to replace target");
585 }
586 }
587 } else {
588 log::error!(
589 "[{}] Target already exists and is not allowed to be replaced",
590 source_path.display()
591 );
592
593 skipped!(&mut self.builder, link, "Link target does already exist");
594 }
595 }
596
597 if !self.options.dry_run {
598 cfg_if! {
599 if #[cfg(unix)] {
600 if let Err(err) = std::os::unix::fs::symlink(source_path, target_path) {
601 log::error!("[{}] Failed to create link", source_path.display());
602
603 failed!(&mut self.builder, link, format!("Failed create link: {err}"));
604 };
605 } else if #[cfg(windows)] {
606 let metadata = match source_path.symlink_metadata() {
607 Ok(m) => m,
608 Err(err) => {
609 log::error!("[{}] Failed to read metadata", source_path.display());
610
611 failed!(&mut self.builder, link, format!("Failed get link source metadata: {err}"));
612 }
613 };
614
615 if metadata.is_dir() {
616 if let Err(err) = std::os::windows::fs::symlink_dir(source_path, target_path) {
617 log::error!("[{}] Failed to create directory link", source_path.display());
618
619 failed!(&mut self.builder, link, format!("Failed create directory link: {err}"));
620 };
621 } else if metadata.is_file() {
622 if let Err(err) = std::os::windows::fs::symlink_file(source_path, target_path) {
623 log::error!("[{}] Failed to create file link", source_path.display());
624
625 failed!(&mut self.builder, link, format!("Failed create file link: {err}"));
626 };
627 } else {
628 log::error!("[{}] Invalid link source type", source_path.display());
629
630 failed!(&mut self.builder, link, "Invalid type of link source");
631 }
632 } else {
633 log::warn!("[{}] Link operations are only supported for unix and windows systems", source_path.display());
634
635 skipped!(&mut self.builder, link, "Link operations are only supported on unix and windows systems");
636 }
637 }
638 }
639
640 success!(&mut self.builder, link);
641
642 Ok(())
643 }
644
645 fn accept_rejected<'a>(
647 &mut self,
648 _: &PunktfSource,
649 _: &LayeredProfile,
650 rejected: &Rejected<'a>,
651 ) -> Result {
652 log::info!(
653 "[{}] Rejected - {}",
654 rejected.relative_source_path.display(),
655 rejected.reason
656 );
657
658 skipped!(&mut self.builder, rejected, rejected.reason.clone());
659 }
660
661 fn accept_errored<'a>(
663 &mut self,
664 _: &PunktfSource,
665 _: &LayeredProfile,
666 errored: &Errored<'a>,
667 ) -> Result {
668 log::error!(
669 "[{}] Failed - {}",
670 errored.relative_source_path.display(),
671 errored
672 );
673
674 failed!(&mut self.builder, errored, errored.to_string());
675 }
676}
677
678impl<F> TemplateVisitor for Deployer<F>
679where
680 F: Fn(&Path, &Path) -> color_eyre::Result<bool>,
681{
682 fn accept_template<'a>(
686 &mut self,
687 _: &PunktfSource,
688 profile: &LayeredProfile,
689 file: &File<'a>,
690 resolve_content: impl FnOnce(&str) -> color_eyre::Result<String>,
693 ) -> Result {
694 log::info!(
695 "[{}] Deploying template",
696 file.relative_source_path.display()
697 );
698
699 let cont = self.pre_deploy_checks(file)?;
700
701 if !cont {
702 return Ok(());
703 }
704
705 let content = match safe_read(&file.source_path) {
706 Ok(SafeRead::Binary(b)) => {
707 log::info!(
708 "[{}] Not evaluated as template - Binary data",
709 file.relative_source_path.display()
710 );
711
712 b
713 }
714 Ok(SafeRead::String(s)) => {
715 let content = match resolve_content(&s) {
716 Ok(content) => content,
717 Err(err) => {
718 log::info!(
719 "[{}] Failed to resolve template",
720 file.relative_source_path.display()
721 );
722
723 failed!(
724 &mut self.builder,
725 file,
726 format!("Failed to resolve template: {err}")
727 );
728 }
729 };
730
731 let Ok(content) = self.transform_content(profile, file, content) else {
732 return Ok(());
734 };
735
736 content.into_bytes()
737 }
738 Err(err) => {
739 log::info!(
740 "[{}] Failed to read file",
741 file.relative_source_path.display()
742 );
743
744 failed!(&mut self.builder, file, format!("Failed to read: {err}"));
745 }
746 };
747
748 if !self.options.dry_run {
749 if let Err(err) = std::fs::write(&file.target_path, content) {
750 log::info!(
751 "[{}] Failed to write content",
752 file.relative_source_path.display()
753 );
754
755 failed!(
756 &mut self.builder,
757 file,
758 format!("Failed to write content: {err}")
759 );
760 }
761 }
762
763 log::info!(
764 "[{}] Template successfully deployed",
765 file.relative_source_path.display()
766 );
767
768 success!(&mut self.builder, file);
769
770 Ok(())
771 }
772}