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 #[clap(short, long)]
23 name: String,
24 #[clap(short, long)]
27 synth_path: Option<String>,
28 #[clap(short, long)]
30 cleanup: bool,
31 },
32 #[clap(about = "Generate diff with a deployed template with the given name")]
33 Diff {
34 #[clap(short, long)]
36 name: String,
37 #[clap(short, long)]
40 synth_path: Option<String>,
41 #[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: String,
49 #[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
136async 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}