angreal/
lib.rs

1//!  Angreal - project templating and task management
2//!
3//!  A package for templating code based projects and providing methods
4//! for the creation and management of common operational tasks associated with the
5//! project.
6//!
7
8#[macro_use]
9extern crate version;
10#[macro_use]
11pub mod macros;
12
13pub mod builder;
14pub mod error_formatter;
15pub mod git;
16pub mod init;
17pub mod logger;
18pub mod py_logger;
19pub mod task;
20pub mod utils;
21pub mod validation;
22
23use builder::build_app;
24use error_formatter::PythonErrorFormatter;
25use task::ANGREAL_TASKS;
26
27use pyo3::types::{IntoPyDict, PyDict};
28use std::ops::Not;
29use std::vec::Vec;
30
31use std::process::exit;
32
33use pyo3::{prelude::*, wrap_pymodule};
34
35use log::{debug, error, warn};
36
37/// The main function is just an entry point to be called from the core angreal library.
38#[pyfunction]
39fn main() -> PyResult<()> {
40    let handle = logger::init_logger();
41    if std::env::var("ANGREAL_DEBUG").unwrap_or_default() == "true" {
42        logger::update_verbosity(&handle, 2);
43        warn!("Angreal application starting with debug level logging from environment");
44    }
45    debug!("Angreal application starting...");
46
47    // because we execute this from python main, we remove the first elements that
48    // IIRC its python and angreal
49    let mut argvs: Vec<String> = std::env::args().collect();
50    argvs = argvs.split_off(2);
51
52    debug!("Checking if binary is up to date...");
53    match utils::check_up_to_date() {
54        Ok(()) => (),
55        Err(e) => warn!(
56            "An error occurred while checking if our binary is up to date. {}",
57            e.to_string()
58        ),
59    };
60
61    // Load any angreal task assets that are available to us
62    let angreal_project_result = utils::is_angreal_project();
63    let in_angreal_project = angreal_project_result.is_ok();
64
65    if in_angreal_project {
66        debug!("Angreal project detected, loading found tasks.");
67        let angreal_path = angreal_project_result.expect("Expected angreal project path");
68        // get a list of files
69        let angreal_tasks_to_load = utils::get_task_files(angreal_path);
70
71        // Explicitly capture error with exit
72        let _angreal_tasks_to_load = match angreal_tasks_to_load {
73            Ok(tasks) => tasks,
74            Err(_) => {
75                error!("Exiting due to unrecoverable error.");
76                exit(1);
77            }
78        };
79
80        // load the files , IF a file has command or task decorators - they'll register themselves now
81        for task in _angreal_tasks_to_load.iter() {
82            if let Err(e) = utils::load_python(task.clone()) {
83                error!("Failed to load Python task: {}", e);
84            }
85        }
86    }
87
88    let app = build_app(in_angreal_project);
89    let mut app_copy = app.clone();
90    let sub_command = app.get_matches_from(&argvs);
91
92    // Get our asked for verbosity and set the logger up. TODO: find a way to initialize earlier and reset after.
93    let verbosity = sub_command.get_count("verbose");
94
95    // If the user hasn't set the ANGREAL_DEBUG environment variable, set the verbosity from CLI settings
96    if std::env::var("ANGREAL_DEBUG").is_err() {
97        logger::update_verbosity(&handle, verbosity);
98        debug!("Log verbosity set to level: {}", verbosity);
99    }
100
101    match sub_command.subcommand() {
102        Some(("init", _sub_matches)) => init::init(
103            _sub_matches.value_of("template").unwrap(),
104            _sub_matches.is_present("force"),
105            _sub_matches.is_present("defaults").not(),
106            if _sub_matches.is_present("values_file") {
107                Some(_sub_matches.value_of("values_file").unwrap())
108            } else {
109                None
110            },
111        ),
112        Some((task, sub_m)) => {
113            if !in_angreal_project {
114                error!("This doesn't appear to be an angreal project.");
115                exit(1)
116            }
117
118            let mut command_groups: Vec<String> = Vec::new();
119            command_groups.push(task.to_string());
120
121            // iterate matches to get our final command and get our final arg matches
122            // object for applying down stream
123            let mut next = sub_m.subcommand();
124            let mut arg_matches = sub_m.clone();
125            while next.is_some() {
126                let cmd = next.unwrap();
127                command_groups.push(cmd.0.to_string());
128                next = cmd.1.subcommand();
129                arg_matches = cmd.1.clone();
130            }
131
132            let task = command_groups.pop().unwrap();
133
134            let some_command = ANGREAL_TASKS.lock().unwrap().clone();
135            let some_command = some_command.iter().find(|&x| {
136                x.name == task.as_str()
137                    && x.group
138                        .clone()
139                        .unwrap()
140                        .iter()
141                        .map(|x| x.name.to_string())
142                        .collect::<Vec<String>>()
143                        == command_groups
144            });
145
146            debug!("Executing command: {}", task);
147            let command = match some_command {
148                None => {
149                    error!("Command '{}' not found.", task);
150                    app_copy.print_help().unwrap_or(());
151                    exit(1)
152                }
153                Some(some_command) => some_command,
154            };
155
156            let args = builder::select_args(task.as_str());
157            Python::with_gil(|py| {
158                debug!("Starting Python execution for command: {}", task);
159                let mut kwargs: Vec<(&str, PyObject)> = Vec::new();
160
161                for arg in args.into_iter() {
162                    let n = Box::leak(Box::new(arg.name));
163                    // unable to find the value of the passed arg with sub_m when its been wrapped
164                    // in a command group
165
166                    if arg.is_flag.unwrap() {
167                        let v = arg_matches.get_flag(&n.clone());
168                        kwargs.push((n.as_str(), v.to_object(py)));
169                    } else {
170                        let v = arg_matches.value_of(n.clone());
171                        match v {
172                            None => {
173                                // We need to handle "boolean flags" that are present w/o a value
174                                // should probably test that the name is a "boolean type also"
175                                kwargs.push((n.as_str(), v.to_object(py)));
176                            }
177                            Some(v) => match arg.python_type.unwrap().as_str() {
178                                "str" => kwargs.push((n.as_str(), v.to_object(py))),
179                                "int" => kwargs
180                                    .push((n.as_str(), v.parse::<i32>().unwrap().to_object(py))),
181                                "float" => kwargs
182                                    .push((n.as_str(), v.parse::<f32>().unwrap().to_object(py))),
183                                _ => kwargs.push((n.as_str(), v.to_object(py))),
184                            },
185                        }
186                    }
187                }
188
189                let r_value = command.func.call(py, (), Some(kwargs.into_py_dict(py)));
190
191                match r_value {
192                    Ok(_r_value) => debug!("Successfully executed Python command: {}", task),
193                    Err(err) => {
194                        error!("Failed to execute Python command: {}", task);
195                        let formatter = PythonErrorFormatter::new(err);
196                        println!("{}", formatter);
197                        exit(1);
198                    }
199                }
200            });
201        }
202        _ => {
203            println!("process for current context")
204        }
205    }
206
207    debug!("Angreal application completed successfully.");
208    Ok(())
209}
210
211#[pymodule]
212fn angreal(_py: Python, m: &PyModule) -> PyResult<()> {
213    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
214
215    py_logger::register();
216    m.add_function(wrap_pyfunction!(main, m)?)?;
217    task::register(_py, m)?;
218    utils::register(_py, m)?;
219
220    m.add_wrapped(wrap_pymodule!(_integrations))?;
221
222    let sys = PyModule::import(_py, "sys")?;
223    let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?;
224    sys_modules.set_item("angreal._integrations", m.getattr("_integrations")?)?;
225    sys_modules.set_item(
226        "angreal._integrations.docker",
227        m.getattr("_integrations")?.getattr("docker")?,
228    )?;
229
230    sys_modules.set_item(
231        "angreal._integrations.docker.image",
232        m.getattr("_integrations")?
233            .getattr("docker")?
234            .getattr("image")?,
235    )?;
236    sys_modules.set_item(
237        "angreal._integrations.docker.container",
238        m.getattr("_integrations")?
239            .getattr("docker")?
240            .getattr("container")?,
241    )?;
242    sys_modules.set_item(
243        "angreal._integrations.docker.network",
244        m.getattr("_integrations")?
245            .getattr("docker")?
246            .getattr("network")?,
247    )?;
248    sys_modules.set_item(
249        "angreal._integrations.docker.volume",
250        m.getattr("_integrations")?
251            .getattr("docker")?
252            .getattr("volume")?,
253    )?;
254    Ok(())
255}
256
257#[pymodule]
258fn _integrations(_py: Python, m: &PyModule) -> PyResult<()> {
259    m.add_wrapped(wrap_pymodule!(docker))?;
260    Ok(())
261}
262
263#[pymodule]
264fn docker(_py: Python, m: &PyModule) -> PyResult<()> {
265    m.add_class::<docker_pyo3::Pyo3Docker>()?;
266    m.add_wrapped(wrap_pymodule!(docker_pyo3::image::image))?;
267    m.add_wrapped(wrap_pymodule!(docker_pyo3::container::container))?;
268    m.add_wrapped(wrap_pymodule!(docker_pyo3::network::network))?;
269    m.add_wrapped(wrap_pymodule!(docker_pyo3::volume::volume))?;
270    Ok(())
271}