Skip to main content

cargo_rusty/
lib.rs

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