Skip to main content

cargo_rusty/
lib.rs

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