Skip to main content

cargo_rusty/
lib.rs

1use std::fs::{read_dir, read_to_string};
2use std::process::exit;
3use clap::Parser;
4use clap::Subcommand;
5use tokio::fs::remove_file;
6use tokio::process::Command;
7use rusty_cdk::{deploy, destroy, diff};
8use rusty_cdk::stack::Stack;
9use rusty_cdk::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
10
11const CURRENT_DIR: &'static str = ".";
12
13#[derive(Clone, Debug, Subcommand)]
14pub enum RustyCommand {
15    #[clap(about = "Deploy a stack")]
16    Deploy {
17        /// Name of the stack when it's deployed
18        #[clap(short, long)]
19        name: String,
20        /// Path of synthesized stack relative to the current directory
21        /// If no path is passed in, the command will generate a synthesized stack using `cargo run` 
22        #[clap(short, long)]
23        synth_path: Option<String>,
24        /// Cleans up the generated or passed-in synth file
25        #[clap(short, long)]
26        cleanup: bool
27    },
28    #[clap(about = "Generate diff with a deployed template with the given name")]
29    Diff {
30        /// Name of the (deployed) stack that you want to compare with
31        #[clap(short, long)]
32        name: String,
33        /// Path of synthesized stack relative to the current directory
34        /// If no path is passed in, the command will generate a synthesized stack using `cargo run`
35        #[clap(short, long)]
36        synth_path: Option<String>,
37        /// Cleans up the generated or passed-in synth file
38        #[clap(short, long, default_missing_value="false")]
39        cleanup: bool
40    },
41    #[clap(about = "Destroy a stack with the give name")]
42    Destroy {
43        /// Name of the (deployed) stack that you want to delete
44        name: String,
45        /// In addition to deleting the stack, `force` will also
46        /// - empty S3 buckets that do not have a 'retain'
47        #[clap(short, long, default_missing_value="false")]
48        force: bool
49    },
50}
51
52#[derive(Parser, Debug)]
53#[command(version, about, long_about = None)]
54pub struct Args {
55    #[command(subcommand)]
56    pub command: RustyCommand,
57}
58
59pub async fn entry_point(command: RustyCommand) {
60    match command {
61        RustyCommand::Deploy { name, synth_path, cleanup } => {
62            println!("deploying stack with name {name}");
63
64            let path = if let Some(path) = synth_path {
65                path
66            } else {
67                match run_synth_in_current_path().await {
68                    Ok(path) => path,
69                    Err(e) => {
70                        eprintln!("{e}");
71                        exit(1);
72                    }
73                }
74            };
75            match get_path_as_stack(&path) {
76                Ok(stack) => deploy(StringWithOnlyAlphaNumericsAndHyphens(name), stack).await,
77                Err(e) => {
78                    eprintln!("{e}");
79                    exit(1);
80                }
81            }
82
83            if cleanup {
84                remove_fill_or_exit(&path).await;
85            }
86        }
87        RustyCommand::Diff { name, synth_path, cleanup } => {
88            println!("creating a diff with an existing stack (name {name})");
89
90            let path = if let Some(path) = synth_path {
91                path
92            } else {
93                match run_synth_in_current_path().await {
94                    Ok(path) => path,
95                    Err(e) => {
96                        eprintln!("{e}");
97                        exit(1);
98                    }
99                }
100            };
101            match get_path_as_stack(&path) {
102                Ok(stack) => diff(StringWithOnlyAlphaNumericsAndHyphens(name), stack).await,
103                Err(e) => {
104                    eprintln!("{e}");
105                    exit(1);
106                }
107            }
108
109            if cleanup {
110                remove_fill_or_exit(&path).await;
111            }
112        }
113        RustyCommand::Destroy { name, force } => {
114            println!("destroying stack with name {name}");
115            destroy(StringWithOnlyAlphaNumericsAndHyphens(name)).await;
116        }
117    }
118}
119
120// alternative to all these matches, an error that implements some Froms
121async fn run_synth_in_current_path() -> Result<String, String> {
122    let dir_content = read_dir(CURRENT_DIR);
123    
124    match dir_content {
125        Ok(content) => {
126            let is_rust_project = content
127                .flat_map(|entry| entry.ok())
128                .any(|entry| {
129                    entry.file_name() == "Cargo.toml" && entry.file_type().map(|f| f.is_file()).unwrap_or(false)
130                });
131
132            if is_rust_project {
133                match Command::new("sh")
134                    .args(&["-c", "cargo run > cargo-rusty-temporary-synth.json"])
135                    .output()
136                    .await {
137                    Ok(_) => Ok("./cargo-rusty-temporary-synth.json".to_string()),
138                    Err(e) => {
139                        Err(format!("Could not run `cargo run` (required to synth when no synth_path is passed in): {e}"))
140                    }
141                }
142            } else {
143                Err("current dir does not seem to be a cargo project, could not find a Cargo.toml (required to synth when no synth_path is passed in)".to_string())
144            }
145        }
146        Err(e) => {
147            Err(format!("could not read dir: {e}"))       
148        }
149    }
150}
151
152fn get_path_as_stack(path: &str) -> Result<Stack, String> {
153    match read_to_string(path) {
154        Ok(as_string) => {
155            match serde_json::from_str::<Stack>(&as_string) {
156                Ok(stack) => Ok(stack),
157                Err(e) => Err(format!("content of file {path} could not be read as a `Stack` (is there non-json content present?): {e}")),
158            }
159        }
160        Err(e) => Err(format!("could not read file with path {path}: {e}")),
161    }
162}
163
164async fn remove_fill_or_exit(path: &String) {
165    let removed = remove_file(&path).await;
166
167    if let Err(e) = removed {
168        eprintln!("error cleaning up file at {path}: {e}");
169        exit(1);
170    }
171}