1use std::process::Command;
2
3use crate::utils::aws::instance_system_log::stream_system_log;
4use crate::{
5 context::Context,
6 prelude::{Environment, EnvironmentName, anyhow::Context as _},
7 utils::{self, aws},
8};
9
10const SSM_SESSION_DOC: &str = "Xtask-Host-InteractiveShell";
11
12#[tracel_xtask_macros::declare_command_args(None, HostSubCommand)]
13pub struct HostCmdArgs {}
14
15impl Default for HostSubCommand {
16 fn default() -> Self {
17 HostSubCommand::Connect(HostConnectSubCmdArgs::default())
18 }
19}
20
21#[derive(clap::Args, Clone, Default, PartialEq)]
22pub struct HostConnectSubCmdArgs {
23 #[arg(long)]
25 pub name: String,
26
27 #[arg(long)]
29 pub region: String,
30
31 #[arg(long, default_value = "ubuntu")]
33 pub user: String,
34
35 #[arg(long)]
37 pub system_log: bool,
38}
39
40#[derive(clap::Args, Clone, Default, PartialEq)]
41pub struct HostPrivateIpSubCmdArgs {
42 #[arg(long)]
44 pub name: String,
45
46 #[arg(long)]
48 pub region: String,
49}
50
51pub fn handle_command(args: HostCmdArgs, env: Environment, _ctx: Context) -> anyhow::Result<()> {
52 if matches!(
53 env.name,
54 EnvironmentName::Development | EnvironmentName::Test
55 ) {
56 anyhow::bail!(
57 "'database' command not supported for environment {env}, use local docker-compose or dev DB instead."
58 );
59 }
60
61 match args.get_command() {
62 HostSubCommand::Connect(connect_args) => connect(connect_args),
63 HostSubCommand::PrivateIp(privateip_args) => private_ip(privateip_args, env),
64 }
65}
66
67fn connect(args: HostConnectSubCmdArgs) -> anyhow::Result<()> {
68 let describe_output = Command::new("aws")
70 .args([
71 "ec2",
72 "describe-instances",
73 "--region",
74 &args.region,
75 "--filters",
76 &format!("Name=tag:Name,Values={}", args.name),
77 "Name=instance-state-name,Values=running",
78 "--query",
79 "Reservations[0].Instances[0].InstanceId",
80 "--output",
81 "text",
82 ])
83 .output()
84 .with_context(|| {
85 format!(
86 "Describing host instance '{}' in region '{}' should succeed",
87 args.name, args.region
88 )
89 })?;
90
91 if !describe_output.status.success() {
92 let stderr = String::from_utf8_lossy(&describe_output.stderr);
93 anyhow::bail!(
94 "Describing host instance '{}' in region '{}' should succeed, but AWS CLI exited with:\n{}",
95 args.name,
96 args.region,
97 stderr
98 );
99 }
100
101 let instance_id = String::from_utf8(describe_output.stdout)
102 .context("Parsing host instance ID from AWS CLI output should succeed")?
103 .trim()
104 .to_string();
105
106 if instance_id.is_empty() || instance_id == "None" {
107 anyhow::bail!(
108 "Finding a running host instance named '{}' in region '{}' should succeed, but none were found",
109 args.name,
110 args.region
111 );
112 }
113
114 if args.system_log {
115 eprintln!(
116 "📜 Streaming system log for host '{}' (id '{}') in region '{}' — Ctrl-C to stop",
117 args.name, instance_id, args.region
118 );
119 return stream_system_log(&args.region, &instance_id);
120 }
121
122 aws::cli::ensure_ssm_document(SSM_SESSION_DOC, &args.region, &args.user)?;
124
125 eprintln!(
126 "🔌 Opening SSM session to host '{}' (id '{}') in region '{}' as user '{}'...",
127 args.name, instance_id, args.region, args.user
128 );
129
130 let args_vec: Vec<&str> = vec![
131 "ssm",
132 "start-session",
133 "--target",
134 instance_id.as_str(),
135 "--region",
136 args.region.as_str(),
137 "--document-name",
138 SSM_SESSION_DOC,
139 ];
140
141 utils::process::run_process(
142 "aws",
143 &args_vec,
144 None,
145 None,
146 "SSM session to host should start successfully",
147 )?;
148
149 Ok(())
150}
151
152fn private_ip(args: HostPrivateIpSubCmdArgs, _env: Environment) -> anyhow::Result<()> {
153 let describe_output = Command::new("aws")
155 .args([
156 "ec2",
157 "describe-instances",
158 "--region",
159 &args.region,
160 "--filters",
161 &format!("Name=tag:Name,Values={}", args.name),
162 "Name=instance-state-name,Values=running",
163 "--query",
164 "Reservations[0].Instances[0].PrivateIpAddress",
165 "--output",
166 "text",
167 ])
168 .output()
169 .with_context(|| format!("Describing host instance '{}' should succeed", args.name))?;
170
171 if !describe_output.status.success() {
172 let stderr = String::from_utf8_lossy(&describe_output.stderr);
173 anyhow::bail!(
174 "Describing host instance '{}' should succeed, but AWS CLI exited with:\n{}",
175 args.name,
176 stderr
177 );
178 }
179
180 let private_ip = String::from_utf8(describe_output.stdout)
182 .context("Parsing host private IP from AWS CLI output should succeed")?
183 .trim()
184 .to_string();
185
186 if private_ip.is_empty() || private_ip == "None" {
187 anyhow::bail!(
188 "Finding a running instance named '{}' should return a private IP address, but none were found",
189 args.name,
190 );
191 }
192
193 println!("{private_ip}");
195
196 Ok(())
197}