rusticity_term/aws/
profile.rs

1use crate::common::SortDirection;
2use crate::ui::filter_area;
3use crate::ui::table::{render_table, Column as TableColumn, TableConfig};
4use ratatui::{prelude::*, widgets::*};
5
6pub struct Profile {
7    pub name: String,
8    pub region: Option<String>,
9    pub account: Option<String>,
10    pub role_arn: Option<String>,
11    pub source_profile: Option<String>,
12}
13
14enum ProfileColumn {
15    Name,
16    Account,
17    Region,
18    Role,
19    Source,
20}
21
22impl TableColumn<Profile> for ProfileColumn {
23    fn name(&self) -> &str {
24        match self {
25            Self::Name => "Profile",
26            Self::Account => "Account",
27            Self::Region => "Region",
28            Self::Role => "Role/User",
29            Self::Source => "Source",
30        }
31    }
32
33    fn width(&self) -> u16 {
34        match self {
35            Self::Name => 25,
36            Self::Account => 15,
37            Self::Region => 15,
38            Self::Role => 30,
39            Self::Source => 20,
40        }
41    }
42
43    fn render(&self, item: &Profile) -> (String, Style) {
44        let text = match self {
45            Self::Name => item.name.clone(),
46            Self::Account => item.account.clone().unwrap_or_default(),
47            Self::Region => item.region.clone().unwrap_or_default(),
48            Self::Role => {
49                if let Some(ref role) = item.role_arn {
50                    if role.contains(":role/") {
51                        let role_name = role.split('/').next_back().unwrap_or(role);
52                        format!("role/{}", role_name)
53                    } else if role.contains(":user/") {
54                        let user_name = role.split('/').next_back().unwrap_or(role);
55                        format!("user/{}", user_name)
56                    } else {
57                        role.clone()
58                    }
59                } else {
60                    String::new()
61                }
62            }
63            Self::Source => item.source_profile.clone().unwrap_or_default(),
64        };
65        (text, Style::default())
66    }
67}
68
69impl Profile {
70    pub fn load_all() -> Vec<Self> {
71        let mut profiles = Vec::new();
72        let home = std::env::var("HOME").unwrap_or_default();
73        let config_path = format!("{}/.aws/config", home);
74        let credentials_path = format!("{}/.aws/credentials", home);
75
76        // Parse config file
77        if let Ok(content) = std::fs::read_to_string(&config_path) {
78            let mut current_profile: Option<String> = None;
79            let mut current_region: Option<String> = None;
80            let mut current_role: Option<String> = None;
81            let mut current_source: Option<String> = None;
82
83            for line in content.lines() {
84                let line = line.trim();
85                if line.starts_with('[') && line.ends_with(']') {
86                    if let Some(name) = current_profile.take() {
87                        profiles.push(Profile {
88                            name,
89                            region: current_region.take(),
90                            account: None,
91                            role_arn: current_role.take(),
92                            source_profile: current_source.take(),
93                        });
94                    }
95                    let profile_name = line
96                        .trim_start_matches('[')
97                        .trim_end_matches(']')
98                        .trim_start_matches("profile ")
99                        .to_string();
100                    current_profile = Some(profile_name);
101                } else if let Some(key_value) = line.split_once('=') {
102                    let key = key_value.0.trim();
103                    let value = key_value.1.trim().to_string();
104                    match key {
105                        "region" => current_region = Some(value),
106                        "role_arn" => current_role = Some(value),
107                        "source_profile" => current_source = Some(value),
108                        _ => {}
109                    }
110                }
111            }
112            if let Some(name) = current_profile {
113                profiles.push(Profile {
114                    name,
115                    region: current_region,
116                    account: None,
117                    role_arn: current_role,
118                    source_profile: current_source,
119                });
120            }
121        }
122
123        // Parse credentials file for additional profiles
124        if let Ok(content) = std::fs::read_to_string(&credentials_path) {
125            for line in content.lines() {
126                let line = line.trim();
127                if line.starts_with('[') && line.ends_with(']') {
128                    let profile_name = line
129                        .trim_start_matches('[')
130                        .trim_end_matches(']')
131                        .to_string();
132                    if !profiles.iter().any(|p| p.name == profile_name) {
133                        profiles.push(Profile {
134                            name: profile_name,
135                            region: None,
136                            account: None,
137                            role_arn: None,
138                            source_profile: None,
139                        });
140                    }
141                }
142            }
143        }
144
145        profiles
146    }
147}
148
149pub fn render_profile_picker(
150    frame: &mut ratatui::Frame,
151    app: &crate::app::App,
152    area: ratatui::prelude::Rect,
153    centered_rect: fn(u16, u16, ratatui::prelude::Rect) -> ratatui::prelude::Rect,
154) {
155    let popup_area = centered_rect(80, 70, area);
156
157    let chunks = Layout::default()
158        .direction(Direction::Vertical)
159        .constraints([Constraint::Length(3), Constraint::Min(0)])
160        .split(popup_area);
161
162    let cursor = "█";
163    let filter_text = vec![
164        Span::raw(&app.profile_filter),
165        Span::styled(cursor, Style::default().fg(Color::Green)),
166    ];
167    let filter = filter_area(filter_text, true);
168
169    frame.render_widget(Clear, popup_area);
170    frame.render_widget(filter, chunks[0]);
171
172    let columns: Vec<Box<dyn TableColumn<Profile>>> = vec![
173        Box::new(ProfileColumn::Name),
174        Box::new(ProfileColumn::Account),
175        Box::new(ProfileColumn::Region),
176        Box::new(ProfileColumn::Role),
177        Box::new(ProfileColumn::Source),
178    ];
179
180    let filtered = app.get_filtered_profiles();
181    let config = TableConfig {
182        items: filtered,
183        selected_index: app.profile_picker_selected,
184        expanded_index: None,
185        columns: &columns,
186        sort_column: "Profile",
187        sort_direction: SortDirection::Asc,
188        title: " Profiles (^R to fetch accounts) ".to_string(),
189        area: chunks[1],
190        get_expanded_content: None,
191        is_active: true,
192    };
193
194    render_table(frame, config);
195}
196
197pub fn filter_profiles<'a>(profiles: &'a [Profile], filter: &str) -> Vec<&'a Profile> {
198    let mut filtered: Vec<&Profile> = if filter.is_empty() {
199        profiles.iter().collect()
200    } else {
201        let filter_lower = filter.to_lowercase();
202        profiles
203            .iter()
204            .filter(|p| {
205                p.name.to_lowercase().contains(&filter_lower)
206                    || p.region
207                        .as_ref()
208                        .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
209                    || p.account
210                        .as_ref()
211                        .is_some_and(|a| a.to_lowercase().contains(&filter_lower))
212                    || p.role_arn
213                        .as_ref()
214                        .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
215                    || p.source_profile
216                        .as_ref()
217                        .is_some_and(|s| s.to_lowercase().contains(&filter_lower))
218            })
219            .collect()
220    };
221    filtered.sort_by(|a, b| a.name.cmp(&b.name));
222    filtered
223}