5
5
6
6
use std:: collections:: HashMap ;
7
7
use std:: io:: { stdin, stdout, Write } ;
8
+ use std:: str:: Utf8Error ;
8
9
9
10
use clap:: { ArgMatches , Parser , FromArgMatches } ;
10
11
@@ -17,6 +18,7 @@ use serde::Serialize;
17
18
use std:: path:: PathBuf ;
18
19
use std:: process:: Stdio ;
19
20
use thiserror:: Error ;
21
+ use tokio:: fs:: try_exists;
20
22
use tokio:: process:: Command ;
21
23
22
24
/// Simple Rust rewrite of a simple Nix Flake deployment tool
@@ -404,7 +406,9 @@ pub enum RunDeployError {
404
406
#[ error( "Failed to revoke profile for node {0}: {1}" ) ]
405
407
RevokeProfile ( String , deploy:: deploy:: RevokeProfileError ) ,
406
408
#[ error( "Deployment to node {0} failed, rolled back to previous generation" ) ]
407
- Rollback ( String )
409
+ Rollback ( String ) ,
410
+ #[ error( "Failed to get the password from sops: {0}" ) ]
411
+ Sops ( #[ from] deploy:: cli:: SopsError ) ,
408
412
}
409
413
410
414
type ToDeploy < ' a > = Vec < (
@@ -548,21 +552,103 @@ async fn run_deploy(
548
552
549
553
let mut deploy_defs = deploy_data. defs ( ) ?;
550
554
551
- if deploy_data. merged_settings . interactive_sudo . unwrap_or ( false ) {
555
+ if deploy_data. merged_settings . sudo . is_some ( )
556
+ && ( deploy_data. merged_settings . interactive_sudo . is_some ( )
557
+ || deploy_data. merged_settings . sudo_secret . is_some ( ) )
558
+ {
559
+ warn ! ( "Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' or 'password File' option. Deployment may fail if the custom command ignores stdin." ) ;
560
+ } else {
561
+ // this configures sudo to hide the password prompt and accept input from stdin
562
+ // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root
563
+ let original = deploy_defs. sudo . unwrap_or ( "sudo" . to_string ( ) ) ;
564
+ deploy_defs. sudo = Some ( format ! ( "{} -S -p \" \" " , original) ) ;
565
+ }
566
+
567
+ if deploy_data
568
+ . merged_settings
569
+ . interactive_sudo
570
+ . unwrap_or ( false )
571
+ {
552
572
warn ! ( "Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\n Please use keys in production environments." ) ;
553
573
554
- if deploy_data. merged_settings . sudo . is_some ( ) {
555
- warn ! ( "Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' option. Deployment may fail if the custom command ignores stdin." ) ;
556
- } else {
557
- // this configures sudo to hide the password prompt and accept input from stdin
558
- // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root
559
- let original = deploy_defs. sudo . unwrap_or ( "sudo" . to_string ( ) ) ;
560
- deploy_defs. sudo = Some ( format ! ( "{} -S -p \" \" " , original) ) ;
561
- }
574
+ info ! (
575
+ "You will now be prompted for the sudo password for {}." ,
576
+ node. node_settings. hostname
577
+ ) ;
578
+
579
+ let sudo_password = rpassword:: prompt_password ( format ! (
580
+ "(sudo for {}) Password: " ,
581
+ node. node_settings. hostname
582
+ ) )
583
+ . unwrap_or ( "" . to_string ( ) ) ;
562
584
563
- info ! ( "You will now be prompted for the sudo password for {}." , node. node_settings. hostname) ;
564
- let sudo_password = rpassword:: prompt_password ( format ! ( "(sudo for {}) Password: " , node. node_settings. hostname) ) . unwrap_or ( "" . to_string ( ) ) ;
585
+ deploy_defs. sudo_password = Some ( sudo_password) ;
586
+ } else if deploy_data. merged_settings . sudo_file . is_some ( )
587
+ && deploy_data. merged_settings . sudo_secret . is_some ( )
588
+ {
589
+ // SAFETY: we already checked if it is some
590
+ let path = deploy_data. merged_settings . sudo_file . clone ( ) . unwrap ( ) ;
591
+ let key = deploy_data. merged_settings . sudo_secret . clone ( ) . unwrap ( ) ;
592
+
593
+ if !try_exists ( & path) . await . unwrap ( ) {
594
+ return Err ( RunDeployError :: Sops ( SopsError :: SopsFileNotFound ( format ! (
595
+ "{path:?} not found"
596
+ ) ) ) ) ;
597
+ }
565
598
599
+ // We deserialze to json
600
+ let out = Command :: new ( "sops" )
601
+ . arg ( "--output-type" )
602
+ . arg ( "json" )
603
+ . arg ( "-d" )
604
+ . arg ( & path)
605
+ . output ( )
606
+ . await
607
+ . map_err ( |err| {
608
+ RunDeployError :: Sops ( SopsError :: SopsFailedDecryption (
609
+ path. to_string_lossy ( ) . into ( ) ,
610
+ err,
611
+ ) )
612
+ } ) ?;
613
+
614
+ let conv_out = std:: str:: from_utf8 ( & out. stdout )
615
+ . map_err ( |err| RunDeployError :: Sops ( SopsError :: SopsCannotConvert ( err) ) ) ?;
616
+
617
+ let mut m: serde_json:: Map < String , serde_json:: Value > = serde_json:: from_str ( conv_out)
618
+ . map_err ( |err| RunDeployError :: Sops ( SopsError :: SerdeDeserialize ( err) ) ) ?;
619
+
620
+ let mut sudo_password = String :: new ( ) ;
621
+
622
+ // We support nested keys like a/b/c
623
+ for i in key. split ( '/' ) {
624
+ match m. get ( i) {
625
+ Some ( v) => match v {
626
+ serde_json:: Value :: String ( s) => {
627
+ sudo_password = s. into ( ) ;
628
+ }
629
+ serde_json:: Value :: Bool ( b) => {
630
+ sudo_password = b. to_string ( ) ;
631
+ }
632
+ serde_json:: Value :: Number ( n) => {
633
+ sudo_password = n. to_string ( ) ;
634
+ }
635
+ serde_json:: Value :: Object ( map) => {
636
+ m = map. clone ( ) ;
637
+ }
638
+ _ => {
639
+ return Err ( RunDeployError :: Sops ( SopsError :: SerdeUnexpectedType (
640
+ "We dont handle Arrays, Bools, None, Numbers" . into ( ) ,
641
+ ) ) ) ;
642
+ }
643
+ } ,
644
+ None => {
645
+ return Err ( RunDeployError :: Sops ( SopsError :: SopsKeyNotFound ( format ! (
646
+ "Did not find {} in Map" ,
647
+ i
648
+ ) ) ) ) ;
649
+ }
650
+ }
651
+ }
566
652
deploy_defs. sudo_password = Some ( sudo_password) ;
567
653
}
568
654
@@ -639,6 +725,22 @@ async fn run_deploy(
639
725
Ok ( ( ) )
640
726
}
641
727
728
+ #[ derive( Error , Debug ) ]
729
+ pub enum SopsError {
730
+ #[ error( "Failed to decrypt file {0}: {1}" ) ]
731
+ SopsFailedDecryption ( String , std:: io:: Error ) ,
732
+ #[ error( "Failed to find sops file: {0}" ) ]
733
+ SopsFileNotFound ( String ) ,
734
+ #[ error( "Failed to convert the output of sops to a str: {0}" ) ]
735
+ SopsCannotConvert ( Utf8Error ) ,
736
+ #[ error( "Failed to deserialize: {0}" ) ]
737
+ SerdeDeserialize ( serde_json:: Error ) ,
738
+ #[ error( "Error unexpected type: {0}" ) ]
739
+ SerdeUnexpectedType ( String ) ,
740
+ #[ error( "Failed to find key: {0}" ) ]
741
+ SopsKeyNotFound ( String ) ,
742
+ }
743
+
642
744
#[ derive( Error , Debug ) ]
643
745
pub enum RunError {
644
746
#[ error( "Failed to deploy profile: {0}" ) ]
0 commit comments