tembo/cli/
context.rs

1use std::fmt::Display;
2use std::fs;
3
4use anyhow::Error;
5use anyhow::{anyhow, bail};
6use serde::Deserialize;
7use serde::Serialize;
8
9use crate::tui;
10
11pub const CONTEXT_EXAMPLE_TEXT: &str = "version = \"1.0\"
12
13[[environment]]
14name = 'local'
15target = 'docker'
16    
17[[environment]]
18name = 'prod'
19target = 'tembo-cloud'
20org_id = 'ORG_ID'
21profile = 'prod'
22set = true";
23
24pub const CREDENTIALS_EXAMPLE_TEXT: &str = "version = \"1.0\"
25    
26[[profile]]
27name = 'prod'
28tembo_access_token = 'ACCESS_TOKEN'
29tembo_host = 'https://api.tembo.io'
30tembo_data_host = 'https://api.data-1.use1.tembo.io'
31";
32
33pub const CONTEXT_DEFAULT_TEXT: &str = "version = \"1.0\"
34
35[[environment]]
36name = 'local'
37target = 'docker'
38set = true
39
40# [[environment]]
41# name = 'prod'
42# target = 'tembo-cloud'
43# org_id can be found in your tembo cloud url. Example: org_2bVDi36rsJNot2gwzP37enwxzMk
44# org_id = 'Org ID here'
45# profile = 'prod'
46";
47
48pub const CREDENTIALS_DEFAULT_TEXT: &str = "version = \"1.0\"
49
50# Remove commented out profile to setup your environment
51
52# [[profile]]
53# name = 'prod'
54# Generate an Access Token either through 'tembo login' or visit 'https://cloud.tembo.io/generate-jwt'
55# tembo_access_token = 'Access token here'
56# tembo_host = 'https://api.tembo.io'
57# tembo_data_host = 'https://api.data-1.use1.tembo.io'
58";
59
60#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
61pub struct Context {
62    pub version: String,
63    pub environment: Vec<Environment>,
64}
65
66// Config struct holds to data from the `[config]` section.
67#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
68pub struct Environment {
69    pub name: String,
70    pub target: String,
71    pub org_id: Option<String>,
72    pub profile: Option<String>,
73    pub set: Option<bool>,
74    pub selected_profile: Option<Profile>,
75}
76
77#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
78pub struct Credential {
79    pub version: String,
80    pub profile: Vec<Profile>,
81}
82
83#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
84pub struct Profile {
85    pub name: String,
86    pub tembo_access_token: String,
87    pub tembo_host: String,
88    pub tembo_data_host: String,
89}
90
91pub enum Target {
92    Docker,
93    TemboCloud,
94}
95
96impl Profile {
97    pub fn get_tembo_data_host(&self) -> String {
98        self.tembo_data_host.trim_end_matches('/').to_string()
99    }
100
101    pub fn get_tembo_host(&self) -> String {
102        self.tembo_host.trim_end_matches('/').to_string()
103    }
104}
105
106impl Display for Target {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        match self {
109            Target::Docker => f.write_str("docker"),
110            Target::TemboCloud => f.write_str("tembo-cloud"),
111        }
112    }
113}
114
115pub fn tembo_home_dir() -> String {
116    let mut tembo_home = home::home_dir().unwrap().as_path().display().to_string();
117    tembo_home.push_str("/.tembo");
118    tembo_home
119}
120
121pub fn tembo_context_file_path() -> String {
122    tembo_home_dir() + "/context"
123}
124
125pub fn tembo_credentials_file_path() -> String {
126    tembo_home_dir() + "/credentials"
127}
128
129pub fn list_context() -> Result<Option<Context>, anyhow::Error> {
130    let filename = tembo_context_file_path();
131
132    let contents = fs::read_to_string(&filename)
133        .map_err(|err| anyhow!("Error reading file {filename}: {err}"))?;
134
135    let maybe_context: Result<Context, toml::de::Error> = toml::from_str(&contents);
136
137    match maybe_context {
138        Ok(mut context) => {
139            let mut count = 0;
140            for e in context.environment.iter_mut() {
141                if e.set == Some(true) {
142                    count += 1;
143                }
144            }
145
146            if count > 1 {
147                bail!("More than one environment is set to true. Check your context file");
148            } else {
149                Ok(Some(context))
150            }
151        }
152        Err(err) => {
153            eprintln!("\nInvalid context file {filename}\n");
154
155            tui::error(&format!("Error: {}", err.message()));
156
157            eprintln!("\nExample context file: \n\n{}", CONTEXT_EXAMPLE_TEXT);
158
159            Err(Error::msg("Error listing tembo context!"))
160        }
161    }
162}
163
164pub fn get_current_context() -> Result<Environment, anyhow::Error> {
165    let maybe_context = list_context()?;
166
167    if let Some(context) = maybe_context {
168        for env in context.environment {
169            if let Some(true) = env.set {
170                if env.name == "local" {
171                    return Ok(env);
172                } else {
173                    let maybe_profiles = list_credential_profiles()?;
174
175                    if let Some(profiles) = maybe_profiles {
176                        if let Some(profile_name) = &env.profile {
177                            let credential = profiles
178                                .iter()
179                                .rev()
180                                .find(|c| &c.name == profile_name)
181                                .ok_or_else(|| anyhow!("Profile not found in credentials"))?;
182
183                            let mut env_with_profile = env.clone();
184                            env_with_profile.selected_profile = Some(credential.clone());
185
186                            return Ok(env_with_profile);
187                        } else {
188                            bail!("Environment is not set up properly. Check out your context");
189                        }
190                    } else {
191                        bail!("Credentials file not found or invalid");
192                    }
193                }
194            }
195        }
196    }
197
198    bail!("Environment is not set up properly. Check out your context");
199}
200
201pub fn list_credential_profiles() -> Result<Option<Vec<Profile>>, anyhow::Error> {
202    let filename = tembo_credentials_file_path();
203
204    let contents = fs::read_to_string(&filename)
205        .map_err(|err| anyhow!("Error reading file {filename}: {err}"))?;
206
207    let maybe_credential: Result<Credential, toml::de::Error> = toml::from_str(&contents);
208
209    match maybe_credential {
210        Ok(credential) => Ok(Some(credential.profile)),
211        Err(err) => {
212            eprintln!("\nInvalid credentials file {filename}\n");
213
214            tui::error(&format!("Error: {}", err.message()));
215
216            eprintln!(
217                "\nExample credentials file: \n\n{}",
218                CREDENTIALS_EXAMPLE_TEXT
219            );
220
221            Err(Error::msg("Error listing tembo credentials profiles!"))
222        }
223    }
224}