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 #[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 #[clap(short, long)]
49 name: String,
50 #[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
132async 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}