Skip to main content

rusticity_term/aws/
profile.rs

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