├── .dockerignore ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ci └── before_deploy.sh └── src ├── app.yaml ├── client ├── elastic.rs ├── fetcher.rs ├── kibana.rs ├── mod.rs └── stub.rs ├── commands ├── base.rs ├── config │ ├── mod.rs │ ├── password_questioner.rs │ ├── resolver.rs │ └── tests │ │ ├── add_server_tests.rs │ │ ├── mocks.rs │ │ ├── mod.rs │ │ ├── show_tests.rs │ │ ├── update_server_tests.rs │ │ └── use_server_tests.rs ├── mod.rs └── search.rs ├── config ├── conf.rs ├── error.rs ├── mod.rs ├── secrets.rs └── server_type.rs ├── display ├── extractor.rs ├── format.rs ├── mod.rs ├── pager │ ├── collector.rs │ ├── mod.rs │ ├── scroll_mode.rs │ └── ui.rs └── renderer.rs ├── error.rs ├── main.rs └── utils ├── mod.rs ├── skip_by_option.rs └── take_by_option.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | .idea 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | 4 | rust: 5 | - stable 6 | 7 | matrix: 8 | include: 9 | - os: osx 10 | rust: stable 11 | env: 12 | - TARGET=x86_64-apple-darwin 13 | - SUFFIX=osx 14 | - os: linux 15 | rust: stable 16 | env: 17 | - TARGET=x86_64-unknown-linux-gnu 18 | - SUFFIX=linux 19 | 20 | addons: 21 | apt: 22 | packages: 23 | - libdbus-1-dev 24 | - libssl-dev 25 | 26 | env: 27 | global: 28 | - PROJECT_NAME=elastic-cli 29 | - TARGET=x86_64-unknown-linux-gnu 30 | - SUFFIX=linux 31 | 32 | before_script: 33 | - rustup component add clippy 34 | 35 | script: 36 | - cargo clippy -- -D warnings 37 | - cargo build --target $TARGET --verbose --release 38 | - cargo test --target $TARGET --verbose 39 | 40 | before_deploy: 41 | - bash ci/before_deploy.sh 42 | 43 | deploy: 44 | - provider: releases 45 | api_key: 46 | secure: "JJpD/sEC1N/v0dyAGNcRmOFx8sQoxOptnZ2fnfGXhQ2Ifjd8+vYNBOadegrAXNnxnqWZtMDgX6XDl6Eo7oM6AYPvLlS5++c/XMZjEiJaUzkCqT2/tEXNnS5Uii9juT1KojAOQkNrVohPFlrE61G0NVF7OWb3KVbKR8L+Mg6K80E4nHmeE1mASk25tpGZ4hCLGTRC7j8rXH2Q9OT5PHhwEyBRjDI+eipZoFyhP/qa/Z19qUjxP6AsMkte8+8itocU58s8bsO+Hbl9R8Ol7TBJ7xgPaKF5Ih3Tq26yWV6othOU0w/ODCNbcEOT8Rx79gSoQ6irKUcY6/URLRdLoJV+JAmw46gH5DM8hy0q7fRVjtu09zBK0bjcqN5wNOggyvzY9KWYqtXYWxSLqPUQBtx0EQAF3hnosxQgF3V32er5AuVcUpuZPhfjVfZeHw7+ZgoAl6HJpn5Wc3HmPyRej6GzDCow9WJDqkOlejhx+pzGscbjTKN2rZmeW0/31xyJrMHN4mrSlCgolhPpV7BlTM6mEYMCiyz+g4mrLyymrXsjZxp06oh/sYmnniFdanW8MdfZYhnG9glh7YtqmOR6WeZaOSEWOzDhRYdEBMZRSab2Oh78PNeuRztPS1ywUUQ+rN3PMilcL2qN+lexislwSJcscbEmUCoNZSrGV/L44UE0Em8=" 47 | file_glob: true 48 | file: $PROJECT_NAME-$TRAVIS_TAG-$SUFFIX.* 49 | skip_cleanup: true 50 | on: 51 | tags: true 52 | condition: $TRAVIS_RUST_VERSION = stable && $TARGET != "" 53 | notifications: 54 | email: 55 | on_success: never -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "elastic-cli" 3 | version = "0.2.5" 4 | authors = ["Prokopev Alexandr "] 5 | 6 | [dependencies] 7 | clap = {version = "2.31.2", features = ["yaml"]} 8 | elastic = "^0.20.10" 9 | elastic_derive = "^0.20.10" 10 | serde = "^1.0" 11 | serde_derive = "^1.0" 12 | serde_json = "^1.0" 13 | serde_yaml = "^0.8.8" 14 | reqwest = "0.9.11" 15 | log = "0.4.1" 16 | stderrlog = "0.3.0" 17 | colored = "1.6" 18 | strfmt = "0.1.6" 19 | dirs = "^1.0.4" 20 | base64 = "^0.10" 21 | failure = "~0.1" 22 | failure_derive = "~0.1" 23 | keyring = "0.6.1" 24 | rpassword = "3.0.1" 25 | termion = "1.5.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Prokopyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch CLI 2 | [![Build Status](https://travis-ci.org/avalarin/elasticsearch-cli.svg?branch=master)](https://travis-ci.org/avalarin/elasticsearch-cli) 3 | [![Coverage Status](https://coveralls.io/repos/github/avalarin/elasticsearch-cli/badge.svg?branch=refactor-config)](https://coveralls.io/github/avalarin/elasticsearch-cli?branch=refactor-config) 4 | 5 | Command-line interface for ElasticSearch 6 | 7 | ## Installing 8 | 9 | ### MacOS 10 | 11 | TBD 12 | 13 | ### Linux 14 | 15 | TBD 16 | 17 | ### From archive 18 | 19 | * Download the latest release from the [releases page](https://github.com/avalarin/elasticsearch-cli/releases/latest) 20 | * Unpack it to the executable files directory (e.g. /usr/local/bin) 21 | * Make the elastic-cli binary executable `chmod +x /usr/local/bin/elastic-cli` 22 | 23 | ## Configuration 24 | 25 | Configuration file stored in your home directory - `~/.elastic-cli`. Empty configuration file will be created at the first lanuch. 26 | 27 | You need to register some elasticsearch server and set is as default: 28 | ``` 29 | elastic-cli config add server local --address http://localhost:9200 --index '*' 30 | elastic-cli config use server local 31 | ``` 32 | 33 | ## Usage 34 | 35 | Examples: 36 | ``` 37 | elastic-cli search -q 'level: Error' 38 | elastic-cli search -q 'level: Error' -o json 39 | elastic-cli search -q 'level: Error' -o '{level} {message}' 40 | elastic-cli search -q 'level: Error' -f 'level,message' 41 | ``` 42 | 43 | For more documentation use help: 44 | ``` 45 | elastic-cli help 46 | elastic-cli search --help 47 | elastic-cli config --help 48 | ``` 49 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | pack() { 6 | local tempdir 7 | local out_dir 8 | local package_name 9 | 10 | tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp) 11 | out_dir=$(pwd) 12 | package_name="$PROJECT_NAME-$TRAVIS_TAG-$SUFFIX" 13 | 14 | # create a "staging" directory 15 | mkdir "$tempdir/$package_name" 16 | 17 | # copying the main binary 18 | cp "target/$TARGET/release/$PROJECT_NAME" "$tempdir/$package_name/" 19 | strip "$tempdir/$package_name/$PROJECT_NAME" 20 | 21 | # manpage, readme and license 22 | # cp "doc/$PROJECT_NAME.1" "$tempdir/$package_name" 23 | cp README.md "$tempdir/$package_name" 24 | # cp LICENSE-MIT "$tempdir/$package_name" 25 | # cp LICENSE-APACHE "$tempdir/$package_name" 26 | 27 | # various autocomplete 28 | # mkdir "$tempdir/$package_name/autocomplete" 29 | # cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".bash "$tempdir/$package_name/autocomplete/${PROJECT_NAME}.bash-completion" 30 | # cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".fish "$tempdir/$package_name/autocomplete" 31 | # cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/_"$PROJECT_NAME" "$tempdir/$package_name/autocomplete" 32 | 33 | # archiving 34 | pushd "$tempdir" 35 | tar czf "$out_dir/$package_name.tar.gz" "$package_name"/* 36 | popd 37 | rm -r "$tempdir" 38 | } 39 | 40 | main() { 41 | pack 42 | } 43 | 44 | main -------------------------------------------------------------------------------- /src/app.yaml: -------------------------------------------------------------------------------- 1 | name: elastic-cli 2 | 3 | args: 4 | - verbosity: 5 | help: "Increase message verbosity" 6 | short: v 7 | multiple: true 8 | - quiet: 9 | help: "Silence all output" 10 | short: q 11 | - config: 12 | help: "Path to the configuration file" 13 | long: config 14 | takes_value: true 15 | - server: 16 | help: "Elasticsearch server name" 17 | long: server 18 | takes_value: true 19 | 20 | subcommands: 21 | - search: 22 | about: "Search logs by the query" 23 | args: 24 | - index: 25 | help: "Elasticsearch index or index pattern" 26 | long: index 27 | short: i 28 | takes_value: true 29 | - query: 30 | help: "Query" 31 | long: query 32 | short: q 33 | takes_value: true 34 | - fields: 35 | help: "Fields" 36 | long: fields 37 | short: f 38 | takes_value: true 39 | - output: 40 | help: "Output format" 41 | long: output 42 | short: o 43 | takes_value: true 44 | - size: 45 | help: "Count of result for fetch" 46 | long: size 47 | default_value: "1000" 48 | takes_value: true 49 | - buffer: 50 | help: "Buffer size" 51 | long: buffer 52 | default_value: "1000" 53 | takes_value: true 54 | - pager: 55 | help: "Enable the pager for output" 56 | long: pager 57 | - config: 58 | about: "Configure" 59 | subcommands: 60 | - add: 61 | subcommands: 62 | - server: 63 | args: 64 | - name: 65 | help: "Server name" 66 | index: 1 67 | - address: 68 | help: "Server address" 69 | long: address 70 | takes_value: true 71 | - index: 72 | help: "Default index" 73 | long: index 74 | takes_value: true 75 | - type: 76 | help: "Type of server (elastic or kibana). Default: elastic" 77 | long: type 78 | takes_value: true 79 | - username: 80 | help: "Optional username for basic auth. If the '--password' option is not specified - the password will be asked" 81 | long: username 82 | takes_value: true 83 | - password: 84 | help: "Optional password for basic auth" 85 | long: password 86 | takes_value: true 87 | - update: 88 | subcommands: 89 | - server: 90 | args: 91 | - name: 92 | help: "Server name" 93 | index: 1 94 | - address: 95 | help: "Server address" 96 | long: address 97 | takes_value: true 98 | - index: 99 | help: "Default index" 100 | long: index 101 | takes_value: true 102 | - type: 103 | help: "Type of server (elastic or kibana)" 104 | long: type 105 | takes_value: true 106 | - username: 107 | help: "Optional username for basic auth. If the '--password' option is not specified - a password will be asked" 108 | long: username 109 | takes_value: true 110 | - password: 111 | help: "Optional password for basic auth" 112 | long: password 113 | takes_value: true 114 | - ask-password: 115 | help: "Force update the password. User name should be specified in the config file, or via the '--username' option" 116 | long: ask-password 117 | - use: 118 | subcommands: 119 | - server: 120 | args: 121 | - name: 122 | help: "Server name" 123 | index: 1 124 | - show: 125 | about: "Show current configuration" -------------------------------------------------------------------------------- /src/client/elastic.rs: -------------------------------------------------------------------------------- 1 | use super::{Client, SearchRequest, ClientError, Fetcher, FetcherError, Collector}; 2 | 3 | use config::{ElasticSearchServer, SecretsReader, Credentials}; 4 | use serde_json::Value; 5 | use elastic::prelude::*; 6 | use elastic::http::header::{Authorization, Basic}; 7 | use elastic::client::SyncSender; 8 | 9 | use std::iter::Iterator; 10 | use std::sync::Arc; 11 | 12 | pub struct ElasticClient { 13 | secrets: Arc, 14 | server_config: ElasticSearchServer, 15 | buffer_size: usize 16 | } 17 | 18 | pub struct ElasticFetcher { 19 | client: elastic::client::Client, 20 | index: String, 21 | query: String, 22 | buffer_size: usize 23 | } 24 | 25 | impl ElasticClient { 26 | pub fn create( 27 | secrets: Arc, 28 | server_config: ElasticSearchServer, 29 | buffer_size: usize 30 | ) -> Self { 31 | ElasticClient { secrets, server_config, buffer_size } 32 | } 33 | } 34 | 35 | impl Client for ElasticClient { 36 | fn execute(&self, request: &SearchRequest) -> Result, ClientError> { 37 | let credentials = self.server_config.username.as_ref() 38 | .map(|username| { 39 | self.secrets.get_credentials(&username).map_err(|err| { 40 | error!("Cannot read credentials: {}", err); 41 | ClientError::RequestError { inner: format!("cannot read credentials: {}", err) } 42 | }) 43 | }) 44 | .unwrap_or_else(|| Ok(None))?; 45 | 46 | // TODO Bearer Token auth 47 | let _token = credentials.clone().map(|Credentials{ username, password }| { 48 | Some(base64::encode(&format!("{}:{}", username, password))) 49 | }); 50 | 51 | let mut builder = SyncClientBuilder::new(); 52 | if let Some(Credentials{ username, password }) = credentials { 53 | builder = builder.params( 54 | |p| { 55 | p.header(Authorization(Basic { 56 | username: username.clone(), 57 | password: Some(password.clone()), 58 | })) 59 | } 60 | ); 61 | } 62 | 63 | let client = builder.base_url(self.server_config.server.clone()) 64 | .build() 65 | .map_err(|err| { 66 | error!("Cannot create elasticsearch client: {}", err); 67 | ClientError::RequestError { inner: format!("{}", err) } 68 | })?; 69 | 70 | let fetcher = ElasticFetcher::create(client, request, self.buffer_size); 71 | 72 | Collector::create(fetcher) 73 | .map_err(From::from) 74 | } 75 | } 76 | 77 | impl Fetcher for ElasticFetcher { 78 | fn fetch_next(&self, from: usize) -> Result<(usize, Vec), FetcherError> { 79 | self.client.search::() 80 | .index(self.index.clone()) 81 | .body(json!({ 82 | "size": self.buffer_size, 83 | "from": from, 84 | "query": { 85 | "query_string" : { 86 | "query" : self.query 87 | } 88 | } 89 | })) 90 | .send() 91 | .map(|resp| (resp.total() as usize, resp.documents().cloned().collect())) 92 | .map_err(|err| { 93 | error!("Cannot read response from elasticsearch: {}", err); 94 | FetcherError::RequestError { inner: format!("cannot read response from elasticsearch:{}", err) } 95 | }) 96 | } 97 | } 98 | 99 | impl ElasticFetcher { 100 | pub fn create( 101 | client: elastic::client::Client, 102 | request: &SearchRequest, 103 | buffer_size: usize 104 | ) -> ElasticFetcher { 105 | ElasticFetcher { 106 | client, 107 | query: request.query.clone(), 108 | index: request.index.clone(), 109 | buffer_size 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/client/fetcher.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Fail)] 2 | pub enum FetcherError { 3 | #[fail(display = "{}", inner)] 4 | RequestError { inner: String } 5 | } 6 | 7 | pub trait Fetcher { 8 | fn fetch_next(&self, from: usize) -> Result<(usize, Vec), FetcherError>; 9 | } 10 | 11 | pub struct Collector where T: Clone { 12 | fetcher: Box>, 13 | buffer: Vec, 14 | pub from: usize, 15 | pub total: usize, 16 | } 17 | 18 | impl Collector where T: Clone { 19 | pub fn create(fetcher: impl Fetcher + 'static) -> Result { 20 | let mut collector = Collector { 21 | fetcher: Box::new(fetcher), 22 | buffer: Vec::new(), 23 | from: 0, 24 | total: 0 25 | }; 26 | 27 | collector.fetch_next(true) 28 | .map(|_| collector) 29 | } 30 | 31 | pub fn iter(&mut self) -> CollectorIterator<'_, T> { 32 | CollectorIterator { 33 | collector: self, 34 | position: 0 35 | } 36 | } 37 | 38 | fn get(&mut self, index: usize) -> Option<&T> { 39 | // no more fetched items 40 | if self.from - index == 0 && 41 | self.fetch_next(false).unwrap_or(0) == 0 { 42 | return None 43 | } 44 | 45 | self.buffer.get(index) 46 | } 47 | 48 | fn fetch_next(&mut self, first: bool) -> Result { 49 | if !first && self.from >= self.total { 50 | return Ok(0) 51 | } 52 | let (total, results) = self.fetcher.fetch_next(self.from)?; 53 | if results.is_empty() { 54 | return Ok(0) 55 | } 56 | let count = results.len(); 57 | info!("Loaded {}/{} results", self.from + count, total); 58 | 59 | self.total = total; 60 | self.from += results.len(); 61 | self.buffer.extend(results); 62 | 63 | Ok(count) 64 | } 65 | } 66 | 67 | pub struct CollectorIterator<'a, T> where T: Clone { 68 | collector: &'a mut Collector, 69 | position: usize 70 | } 71 | 72 | impl <'a, T> Iterator for CollectorIterator<'a, T> where T: Clone { 73 | type Item = T; 74 | 75 | fn next(&mut self) -> Option { 76 | let result = self.collector.get(self.position); 77 | self.position += 1; 78 | result.map(Clone::clone) 79 | } 80 | } 81 | 82 | impl <'a, T> DoubleEndedIterator for CollectorIterator<'a, T> where T: Clone { 83 | fn next_back(&mut self) -> Option { 84 | self.position -= 1; 85 | self.collector.get(self.position).map(Clone::clone) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | struct FnFetcher(pub Box Result<(usize, Vec), FetcherError>>); 94 | impl Fetcher for FnFetcher { 95 | fn fetch_next(&self, from: usize) -> Result<(usize, Vec), FetcherError> { 96 | (self.0)(from) 97 | } 98 | } 99 | 100 | #[test] 101 | fn it_should_return_error_from_fetcher_on_creation() { 102 | let fetcher = FnFetcher::(Box::new(|_from| { 103 | Err(FetcherError::RequestError { inner: "fail".to_string() }) 104 | })); 105 | match Collector::create(fetcher) { 106 | Ok(_) => assert!(false, "creation should be failed"), 107 | Err(_) => assert!(true), 108 | } 109 | } 110 | 111 | #[test] 112 | fn it_should_stop_fetching_when_collector_returned_error() { 113 | let fetcher = FnFetcher(Box::new(|from| match from { 114 | 0 => Ok((5, vec![99, 98])), 115 | 2 => Err(FetcherError::RequestError { inner: "fail".to_string() }), 116 | _ => Ok((5, vec![])), 117 | })); 118 | let result: Vec = Collector::create(fetcher).unwrap().iter().collect(); 119 | assert_eq!(vec![99, 98], result); 120 | } 121 | 122 | #[test] 123 | fn it_should_not_fetch_more_items_than_returned_in_total_count() { 124 | let fetcher = FnFetcher(Box::new(|from| match from { 125 | 0 => Ok((5, vec![99])), 126 | 1 => Ok((5, vec![98, 97])), 127 | 2 => Ok((5, vec![1, 2])), 128 | 3 => Ok((5, vec![96, 95])), 129 | 5 => Ok((5, vec![1, 2])), 130 | _ => Ok((5, vec![1, 2])), 131 | })); 132 | let result: Vec = Collector::create(fetcher).unwrap().iter().collect(); 133 | assert_eq!(vec![99, 98, 97, 96, 95], result); 134 | } 135 | 136 | #[test] 137 | fn it_may_contain_more_items_than_total_count_if_fetcher_returns() { 138 | let fetcher = FnFetcher(Box::new(|from| match from { 139 | 0 => Ok((5, vec![99])), 140 | 1 => Ok((5, vec![98, 97])), 141 | 2 => Ok((5, vec![1, 2])), 142 | 3 => Ok((5, vec![96, 95, 94])), 143 | 5 => Ok((5, vec![1, 2])), 144 | 6...20 => Ok((5, vec![1, 2])), 145 | _ => Ok((5, vec![])) 146 | })); 147 | let result: Vec = Collector::create(fetcher).unwrap().iter().collect(); 148 | assert_eq!(vec![99, 98, 97, 96, 95, 94], result); 149 | } 150 | 151 | #[test] 152 | fn it_should_not_fetch_more_items_than_requested() { 153 | let fetcher = FnFetcher(Box::new(|from| match from { 154 | 0 => Ok((5, vec![99])), 155 | 1 => Ok((5, vec![98, 97])), 156 | _ => panic!("should not happen"), 157 | })); 158 | let result: Vec = Collector::create(fetcher).unwrap().iter().take(3).collect(); 159 | assert_eq!(vec![99, 98, 97], result); 160 | } 161 | 162 | #[test] 163 | fn it_should_fetch_no_more_than_necessary() { 164 | let fetcher = FnFetcher(Box::new(|from| match from { 165 | 0 => Ok((5, vec![99])), 166 | 1 => Ok((5, vec![98, 97])), 167 | _ => panic!("should not happen"), 168 | })); 169 | 170 | let result: Vec = Collector::create(fetcher).unwrap().iter().skip(1).take(2).collect(); 171 | assert_eq!(vec![98, 97], result); 172 | } 173 | 174 | #[test] 175 | fn it_should_fetch_no_less_than_necessary() { 176 | let fetcher = FnFetcher(Box::new(|from| match from { 177 | 0 => Ok((5, vec![99])), 178 | 1 => Ok((5, vec![98, 97])), 179 | _ => panic!("should not happen"), 180 | })); 181 | 182 | let mut collector = Collector::create(fetcher).unwrap(); 183 | 184 | let result: Vec = collector.iter().take(2).collect(); 185 | assert_eq!(vec![99, 98], result); 186 | 187 | let result: Vec = collector.iter().skip(1).take(2).collect(); 188 | assert_eq!(vec![98, 97], result); 189 | } 190 | } -------------------------------------------------------------------------------- /src/client/kibana.rs: -------------------------------------------------------------------------------- 1 | use super::{Client, SearchRequest, ClientError, Fetcher, FetcherError, Collector}; 2 | 3 | use config::{ElasticSearchServer, SecretsReader, Credentials}; 4 | use serde_json::Value; 5 | use elastic::prelude::SearchResponse; 6 | use reqwest::Url; 7 | use std::sync::Arc; 8 | 9 | pub struct KibanaProxyClient { 10 | secrets: Arc, 11 | server_config: ElasticSearchServer, 12 | buffer_size: usize 13 | } 14 | 15 | pub struct KibanaProxyFetcher { 16 | url: Url, 17 | credentials: Option, 18 | client: reqwest::Client, 19 | query: String, 20 | buffer_size: usize 21 | } 22 | 23 | impl KibanaProxyClient { 24 | pub fn create( 25 | secrets: Arc, 26 | server_config: ElasticSearchServer, 27 | buffer_size: usize 28 | ) -> Self { 29 | KibanaProxyClient { 30 | secrets, 31 | server_config, 32 | buffer_size 33 | } 34 | } 35 | } 36 | 37 | impl Client for KibanaProxyClient { 38 | fn execute(&self, request: &SearchRequest) -> Result, ClientError> { 39 | let client = reqwest::Client::new(); 40 | 41 | let mut url = Url::parse_with_params( 42 | self.server_config.server.clone().as_ref(), 43 | vec![("method", "POST"), ("path", format!("{}/_search", request.index).as_ref())] 44 | ).map_err(|err| { 45 | error!("Invalid server address: {}", err); 46 | ClientError::RequestError { inner: format!("invalid server address: {}", err) } 47 | })?; 48 | url.set_path("/api/console/proxy"); 49 | 50 | let credentials = self.server_config.username.as_ref() 51 | .map(|username| { 52 | self.secrets.get_credentials(&username).map_err(|err| { 53 | error!("Cannot read credentials: {}", err); 54 | ClientError::RequestError { inner: format!("cannot read credentials: {}", err) } 55 | }) 56 | }) 57 | .unwrap_or_else(|| Ok(None))?; 58 | 59 | 60 | let fetcher = KibanaProxyFetcher::create(url, credentials, client, request, self.buffer_size); 61 | 62 | Collector::create(fetcher) 63 | .map_err(From::from) 64 | } 65 | } 66 | 67 | impl KibanaProxyFetcher { 68 | pub fn create( 69 | url: Url, 70 | credentials: Option, 71 | client: reqwest::Client, 72 | request: &SearchRequest, 73 | buffer_size: usize 74 | ) -> KibanaProxyFetcher { 75 | KibanaProxyFetcher { 76 | url, 77 | credentials, 78 | client, 79 | query: request.query.clone(), 80 | buffer_size 81 | } 82 | } 83 | } 84 | 85 | impl Fetcher for KibanaProxyFetcher { 86 | fn fetch_next(&self, from: usize) -> Result<(usize, Vec), FetcherError> { 87 | let mut request = self.client.post(self.url.clone()); 88 | 89 | if let Some(Credentials { username, password }) = &self.credentials { 90 | request = request.basic_auth(username.to_owned(), Some(password.to_owned())); 91 | } 92 | 93 | request 94 | .header("kbn-xsrf", "reporting") 95 | .body(json!({ 96 | "size": self.buffer_size, 97 | "from": from, 98 | "query": { 99 | "query_string" : { 100 | "query" : self.query 101 | } 102 | } 103 | }).to_string()) 104 | .send() 105 | .map_err(|err| { 106 | error!("Cannot read response from kibana: {}", err); 107 | FetcherError::RequestError { inner: format!("cannot read response from kibana: {}", err) } 108 | }) 109 | .and_then(|resp| { 110 | if resp.status().is_success() { 111 | Ok(resp) 112 | } else { 113 | error!("Kibana responded {}", resp.status()); 114 | Err(FetcherError::RequestError { inner: format!("kibana responded {}", resp.status()) }) 115 | } 116 | }) 117 | .and_then(|mut resp| { 118 | resp.json::>() 119 | .map_err(|err| { 120 | error!("Cannot parse json response from kibana: {}", err); 121 | FetcherError::RequestError { inner: format!("cannot parse json response from kibana: {}", err) } 122 | }) 123 | }) 124 | .map(|resp| { 125 | (resp.total() as usize, resp.documents().cloned().collect()) 126 | }) 127 | 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | mod fetcher; 2 | pub mod elastic; 3 | pub mod kibana; 4 | pub mod stub; 5 | 6 | pub use self::fetcher::*; 7 | 8 | pub struct SearchRequest { 9 | pub index: String, 10 | pub query: String 11 | } 12 | 13 | pub trait Client { 14 | fn execute(&self, request: &SearchRequest) -> Result, ClientError>; 15 | } 16 | 17 | #[derive(Debug, Fail)] 18 | pub enum ClientError { 19 | #[fail(display = "{}", inner)] 20 | RequestError { inner: String } 21 | } 22 | 23 | impl From for ClientError { 24 | fn from(err: FetcherError) -> Self { 25 | match err { 26 | FetcherError::RequestError { inner } => ClientError::RequestError { inner: inner.clone() } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/client/stub.rs: -------------------------------------------------------------------------------- 1 | use client::{Client, SearchRequest, Collector, ClientError, FetcherError}; 2 | use serde_json::Value; 3 | use client::fetcher::Fetcher; 4 | 5 | pub struct StubClient { 6 | buffer_size: usize 7 | } 8 | 9 | pub struct StubFetcher { 10 | buffer_size: usize, 11 | total_count: usize 12 | } 13 | 14 | impl StubClient { 15 | pub fn new( 16 | buffer_size: usize 17 | ) -> Self { 18 | Self { buffer_size } 19 | } 20 | } 21 | 22 | impl Client for StubClient { 23 | fn execute(&self, _: &SearchRequest) -> Result, ClientError> { 24 | Collector::create(StubFetcher::new(self.buffer_size, 1000)) 25 | .map_err(From::from) 26 | } 27 | } 28 | 29 | impl StubFetcher { 30 | fn new( 31 | buffer_size: usize, 32 | total_count: usize 33 | ) -> Self { 34 | Self { 35 | buffer_size, 36 | total_count 37 | } 38 | } 39 | } 40 | 41 | impl Fetcher for StubFetcher { 42 | 43 | fn fetch_next(&self, from: usize) -> Result<(usize, Vec), FetcherError> { 44 | std::thread::sleep(std::time::Duration::from_millis(200)); 45 | let to = std::cmp::min(from + self.buffer_size, self.total_count); 46 | Ok((self.total_count, (from..to).map(|i| json!({ 47 | "index": i, 48 | "pow": i * i, 49 | "name": format!("Item #{}", i) 50 | })).collect())) 51 | } 52 | } -------------------------------------------------------------------------------- /src/commands/base.rs: -------------------------------------------------------------------------------- 1 | use crate::ApplicationError; 2 | 3 | pub trait Command { 4 | fn execute(&mut self) -> Result<(), ApplicationError>; 5 | } -------------------------------------------------------------------------------- /src/commands/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod resolver; 2 | mod password_questioner; 3 | 4 | #[cfg(test)] 5 | mod tests; 6 | 7 | pub use self::resolver::*; 8 | pub use self::password_questioner::*; 9 | 10 | use clap::{ArgMatches}; 11 | use commands::{Command}; 12 | use config::{ApplicationConfig, ElasticSearchServerType, SecretsWriter}; 13 | use serde_yaml; 14 | use error::ApplicationError; 15 | 16 | use std::str::FromStr; 17 | use std::sync::Arc; 18 | 19 | pub struct ConfigCommand { 20 | pub config: ApplicationConfig, 21 | pub action: ConfigAction, 22 | pub resolver: ConfigActionResolver 23 | } 24 | 25 | #[derive(Clone)] 26 | pub enum ConfigAction { 27 | AddServer { 28 | name: String, 29 | address: String, 30 | server_type: ElasticSearchServerType, 31 | index: Option, 32 | username: Option, 33 | password: Option 34 | }, 35 | UpdateServer { 36 | name: String, 37 | address: Option, 38 | server_type: Option, 39 | index: Option, 40 | username: Option, 41 | password: Option, 42 | ask_password: bool 43 | }, 44 | UseServer { name: String }, 45 | Show 46 | } 47 | 48 | impl ConfigCommand { 49 | pub fn new(config: ApplicationConfig, secrets: Arc, action: ConfigAction) -> Self { 50 | ConfigCommand { 51 | config, 52 | action, 53 | resolver: ConfigActionResolver::new( 54 | Arc::new(TtyPasswordQuestioner::new()), 55 | secrets 56 | ) 57 | } 58 | } 59 | 60 | pub fn parse(config: ApplicationConfig, secrets: Arc, args: &ArgMatches) -> Result { 61 | let action = match args.subcommand() { 62 | ("add", Some(add_match)) => { 63 | match add_match.subcommand() { 64 | ("server", Some(server_match)) => { 65 | let name = server_match.value_of("name").ok_or_else(|| { 66 | error!("Argument 'name' is required"); 67 | ApplicationError 68 | })?; 69 | let address = server_match.value_of("address").ok_or_else(|| { 70 | error!("Argument 'address' is required"); 71 | ApplicationError 72 | })?; 73 | let index = server_match.value_of("index"); 74 | let username = server_match.value_of("username"); 75 | let password = server_match.value_of("password"); 76 | let server_type = server_match.value_of("type") 77 | .map(FromStr::from_str) 78 | .unwrap_or(Ok(ElasticSearchServerType::Elastic)) 79 | .map_err(|err| { 80 | error!("{}", err); 81 | ApplicationError 82 | })?; 83 | Ok(ConfigAction::AddServer { 84 | name: name.to_owned(), 85 | address: address.to_owned(), 86 | server_type, 87 | index: index.map(str::to_owned), 88 | username: username.map(str::to_owned), 89 | password: password.map(str::to_owned), 90 | }) 91 | }, 92 | (resource, _) => { 93 | error!("Unknown resource - {}", resource); 94 | Err(ApplicationError) 95 | } 96 | } 97 | } 98 | ("update", Some(update_match)) => { 99 | match update_match.subcommand() { 100 | ("server", Some(server_match)) => { 101 | let name = server_match.value_of("name").ok_or_else(|| { 102 | error!("Argument 'name' is required"); 103 | ApplicationError 104 | })?; 105 | let address = server_match.value_of("address"); 106 | let index = server_match.value_of("index"); 107 | let username = server_match.value_of("username"); 108 | let password = server_match.value_of("password"); 109 | let ask_password = server_match.is_present("ask-password"); 110 | let server_type = server_match.value_of("type") 111 | .map(ElasticSearchServerType::from_str) 112 | .map_or(Ok(None), |v| v.map(Some)) 113 | .map_err(|err| { 114 | error!("{}", err); 115 | ApplicationError 116 | })?; 117 | 118 | Ok(ConfigAction::UpdateServer { 119 | name: name.to_owned(), 120 | address: address.map(str::to_owned), 121 | server_type, 122 | index: index.map(str::to_owned), 123 | username: username.map(str::to_owned), 124 | password: password.map(str::to_owned), 125 | ask_password 126 | }) 127 | }, 128 | (resource, _) => { 129 | error!("Unknown resource - {}", resource); 130 | Err(ApplicationError) 131 | } 132 | } 133 | } 134 | ("use", Some(use_match)) => { 135 | match use_match.subcommand() { 136 | ("server", Some(server_match)) => { 137 | let name = server_match.value_of("name").ok_or_else(|| { 138 | error!("Argument 'name' is required"); 139 | ApplicationError 140 | })?; 141 | Ok(ConfigAction::UseServer { name: name.to_owned() }) 142 | } 143 | (resource, _) => { 144 | error!("Unknown resource - {}", resource); 145 | Err(ApplicationError) 146 | } 147 | } 148 | } 149 | ("show", _) => Ok(ConfigAction::Show), 150 | (action, _) => { 151 | error!("Unknown configuration action - {}", action); 152 | Err(ApplicationError) 153 | } 154 | }?; 155 | Ok(ConfigCommand::new(config, secrets, action)) 156 | } 157 | } 158 | 159 | impl Command for ConfigCommand { 160 | fn execute(&mut self) -> Result<(), ApplicationError> { 161 | let new_config = self.resolver.resolve(self.action.clone(), self.config.clone()) 162 | .map_err(|err| { 163 | error!("Cannot perform action: {}", err); 164 | ApplicationError 165 | })?; 166 | 167 | info!("Saving new config to file {}", new_config.file_path); 168 | println!("{}\n{}", new_config.file_path, serde_yaml::to_string(&new_config) 169 | .map_err(|err| { 170 | error!("Can't serialize configuration: {}", err); 171 | ApplicationError 172 | })?); 173 | 174 | new_config.save_file() 175 | .map_err(|err| { 176 | error!("Can't save configuration: {}", err); 177 | ApplicationError 178 | }) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/commands/config/password_questioner.rs: -------------------------------------------------------------------------------- 1 | use rpassword::read_password_from_tty; 2 | 3 | #[derive(Debug, Fail, PartialEq)] 4 | #[fail(display = "{}", inner)] 5 | pub struct PasswordQuestionerError { 6 | inner: String 7 | } 8 | 9 | pub trait PasswordQuestioner { 10 | fn ask_password(&self, username: &str) -> Result; 11 | } 12 | 13 | pub struct TtyPasswordQuestioner { 14 | 15 | } 16 | 17 | impl TtyPasswordQuestioner { 18 | pub fn new() -> Self { 19 | Self {} 20 | } 21 | } 22 | 23 | impl PasswordQuestioner for TtyPasswordQuestioner { 24 | fn ask_password(&self, username: &str) -> Result { 25 | read_password_from_tty(Some(&format!("Enter the password for the user {}: ", username))) 26 | .map_err(|err| { 27 | PasswordQuestionerError { inner: format!("{}", err) } 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /src/commands/config/resolver.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | ConfigAction, ApplicationConfig, PasswordQuestioner 3 | }; 4 | 5 | use crate::config::{ElasticSearchServer, SecretsWriter, WriteSecretError}; 6 | 7 | use std::sync::Arc; 8 | use commands::config::password_questioner::PasswordQuestionerError; 9 | 10 | pub struct ConfigActionResolver { 11 | password_questioner: Arc, 12 | secrets_writer: Arc 13 | } 14 | 15 | #[derive(Debug, Fail, PartialEq)] 16 | pub enum ConfigActionError { 17 | #[fail(display = "server {} already exists", server_name)] 18 | ServerAlreadyExists { server_name: String }, 19 | #[fail(display = "server {} does not exists", server_name)] 20 | ServerDoesNotExists { server_name: String }, 21 | #[fail(display = "username should be specified, use --username")] 22 | UsernameShouldBeSpecified, 23 | #[fail(display = "cannot save password: {}", inner)] 24 | CannotSavePassword { inner: WriteSecretError }, 25 | #[fail(display = "{}", inner)] 26 | CannotRetrievePassword { inner: PasswordQuestionerError }, 27 | } 28 | 29 | impl ConfigActionResolver { 30 | pub fn new( 31 | password_questioner: Arc, 32 | secrets_writer: Arc 33 | ) -> Self { 34 | Self { 35 | password_questioner, 36 | secrets_writer 37 | } 38 | } 39 | 40 | pub fn resolve(&self, action: ConfigAction, mut config: ApplicationConfig) -> Result { 41 | match action { 42 | ConfigAction::AddServer { 43 | name, 44 | address, 45 | server_type, 46 | index, 47 | username, 48 | password 49 | } => { 50 | if config.servers.iter().any(|server| server.name == name) { 51 | return Err(ConfigActionError::ServerAlreadyExists { server_name: name }) 52 | } 53 | if config.default_server.is_none() { 54 | config.default_server = Some(name.clone()); 55 | } 56 | config.servers.push(ElasticSearchServer { 57 | name, 58 | server: address, 59 | server_type, 60 | default_index: index, 61 | username: username.clone() 62 | }); 63 | 64 | let password_needed = username.is_some(); 65 | if let Some((username, password)) = self.fetch_credentials(username, password, password_needed)? { 66 | info!("Saving password to the system keychain..."); 67 | self.secrets_writer.write(&username, &password) 68 | .map_err(|err| { 69 | ConfigActionError::CannotSavePassword { inner: err } 70 | })?; 71 | } 72 | } 73 | ConfigAction::UpdateServer { 74 | name, 75 | address, 76 | server_type, 77 | index, 78 | username, 79 | password, 80 | ask_password 81 | } => { 82 | let mut server = config.servers.iter_mut().find(|server| server.name == name) 83 | .ok_or_else(|| { 84 | ConfigActionError::ServerDoesNotExists { server_name: name } 85 | })?; 86 | 87 | if let Some(addr) = address { 88 | server.server = addr 89 | } 90 | if let Some(server_type) = server_type { 91 | server.server_type = server_type 92 | } 93 | if index.is_some() { 94 | server.default_index = index; 95 | } 96 | if username.is_some() { 97 | server.username = username.clone(); 98 | } 99 | 100 | let password_needed = username.is_some() || ask_password; 101 | if let Some((username, password)) = self.fetch_credentials(server.username.clone(), password, password_needed)? { 102 | info!("Saving password to the system keychain..."); 103 | self.secrets_writer.write(&username, &password) 104 | .map_err(|err| { 105 | ConfigActionError::CannotSavePassword { inner: err } 106 | })?; 107 | } 108 | } 109 | ConfigAction::UseServer { name } => { 110 | if config.servers.iter().find(|server| server.name == name).is_none() { 111 | return Err(ConfigActionError::ServerDoesNotExists { server_name: name }) 112 | } 113 | config.default_server = Some(name); 114 | } 115 | ConfigAction::Show => {} 116 | }; 117 | 118 | Ok(config) 119 | } 120 | 121 | fn fetch_credentials( 122 | &self, 123 | username: Option, 124 | password: Option, 125 | ask_password: bool 126 | ) -> Result, ConfigActionError> { 127 | match (username, password, ask_password) { 128 | (Some(username), Some(password), _) => Ok(Some((username, password))), 129 | (Some(username), None, true) => { 130 | self.password_questioner.ask_password(&username) 131 | .map(|p| Some((username, p))) 132 | .map_err(|err| { 133 | ConfigActionError::CannotRetrievePassword { inner: err } 134 | }) 135 | }, 136 | (None, Some(_), _) | (None, None, true) => { 137 | Err(ConfigActionError::UsernameShouldBeSpecified) 138 | }, 139 | (_, None, false) => Ok(None) 140 | } 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /src/commands/config/tests/add_server_tests.rs: -------------------------------------------------------------------------------- 1 | use super::{create_config, create_resolver}; 2 | 3 | use config::{ElasticSearchServer, ElasticSearchServerType}; 4 | use commands::config::resolver::{ConfigActionError}; 5 | 6 | use commands::ConfigAction; 7 | 8 | #[test] 9 | fn should_not_creates_2_servers_with_same_name() { 10 | let mut config = create_config(); 11 | let (resolver, _, _) = create_resolver(); 12 | 13 | config.servers.push(ElasticSearchServer { 14 | name: "test".to_string(), 15 | server: "".to_string(), 16 | server_type: ElasticSearchServerType::Elastic, 17 | default_index: None, 18 | username: None 19 | }); 20 | 21 | let result = resolver.resolve(ConfigAction::AddServer { 22 | name: "test".to_string(), 23 | address: "".to_string(), 24 | server_type: ElasticSearchServerType::Elastic, 25 | index: None, 26 | username: None, 27 | password: None 28 | }, config); 29 | 30 | assert_eq!(Err(ConfigActionError::ServerAlreadyExists { server_name: "test".to_string() }), result); 31 | } 32 | 33 | #[test] 34 | fn should_sets_default_server_if_its_not_set() { 35 | let config = create_config(); 36 | let (resolver, _, _) = create_resolver(); 37 | 38 | let new_config = resolver.resolve(ConfigAction::AddServer { 39 | name: "test".to_string(), 40 | address: "".to_string(), 41 | server_type: ElasticSearchServerType::Elastic, 42 | index: None, 43 | username: None, 44 | password: None 45 | }, config).unwrap(); 46 | 47 | assert_eq!(Some("test".to_string()), new_config.default_server); 48 | } 49 | 50 | #[test] 51 | fn should_puts_new_server() { 52 | let config = create_config(); 53 | let (resolver, _, _) = create_resolver(); 54 | 55 | let new_config = resolver.resolve(ConfigAction::AddServer { 56 | name: "test".to_string(), 57 | address: "address".to_string(), 58 | server_type: ElasticSearchServerType::Kibana, 59 | index: Some("index".to_string()), 60 | username: Some("username".to_string()), 61 | password: None 62 | }, config).unwrap(); 63 | 64 | assert_eq!(vec![ 65 | ElasticSearchServer { 66 | name: "test".to_string(), 67 | server: "address".to_string(), 68 | server_type: ElasticSearchServerType::Kibana, 69 | default_index: Some("index".to_string()), 70 | username: Some("username".to_string()) 71 | } 72 | ], new_config.servers); 73 | } 74 | 75 | #[test] 76 | fn should_asks_for_password_if_username_is_present() { 77 | let config = create_config(); 78 | let (resolver, password, secrets) = create_resolver(); 79 | 80 | resolver.resolve(ConfigAction::AddServer { 81 | name: "test".to_string(), 82 | address: "address".to_string(), 83 | server_type: ElasticSearchServerType::Kibana, 84 | index: Some("index".to_string()), 85 | username: Some("username".to_string()), 86 | password: None 87 | }, config).unwrap(); 88 | 89 | assert_eq!(true, password.was_asked()); 90 | secrets.assert_check("asked_password".to_string()); 91 | } 92 | 93 | #[test] 94 | fn should_not_asks_for_password_if_username_is_not_present() { 95 | let config = create_config(); 96 | let (resolver, password, secrets) = create_resolver(); 97 | 98 | resolver.resolve(ConfigAction::AddServer { 99 | name: "test".to_string(), 100 | address: "address".to_string(), 101 | server_type: ElasticSearchServerType::Kibana, 102 | index: Some("index".to_string()), 103 | username: None, 104 | password: None 105 | }, config).unwrap(); 106 | 107 | assert_eq!(false, password.was_asked()); 108 | secrets.assert_check("".to_string()); 109 | } 110 | 111 | #[test] 112 | fn should_not_asks_for_password_if_password_is_present() { 113 | let config = create_config(); 114 | let (resolver, password, secrets) = create_resolver(); 115 | 116 | resolver.resolve(ConfigAction::AddServer { 117 | name: "test".to_string(), 118 | address: "address".to_string(), 119 | server_type: ElasticSearchServerType::Kibana, 120 | index: Some("index".to_string()), 121 | username: Some("username".to_string()), 122 | password: Some("password".to_string()) 123 | }, config).unwrap(); 124 | 125 | assert_eq!(false, password.was_asked()); 126 | secrets.assert_check("password".to_string()); 127 | } 128 | 129 | #[test] 130 | fn should_fails_when_password_is_specified_but_username_is_not() { 131 | let config = create_config(); 132 | let (resolver, _password, _secrets) = create_resolver(); 133 | 134 | let result = resolver.resolve(ConfigAction::AddServer { 135 | name: "test".to_string(), 136 | address: "address".to_string(), 137 | server_type: ElasticSearchServerType::Kibana, 138 | index: Some("index".to_string()), 139 | username: None, 140 | password: Some("password".to_string()) 141 | }, config); 142 | 143 | assert_eq!(Err(ConfigActionError::UsernameShouldBeSpecified), result); 144 | } -------------------------------------------------------------------------------- /src/commands/config/tests/mocks.rs: -------------------------------------------------------------------------------- 1 | use config::{SecretsWriter, WriteSecretError}; 2 | use commands::config::{PasswordQuestioner, PasswordQuestionerError}; 3 | 4 | use std::cell::{Cell, RefCell}; 5 | use commands::config::resolver::ConfigActionResolver; 6 | use std::sync::Arc; 7 | 8 | pub fn create_resolver() -> (ConfigActionResolver, Arc, Arc) { 9 | let secrets = Arc::new(TestSecrets::new()); 10 | let questioner = Arc::new(TestPasswordQuestioner::new()); 11 | let resolver = ConfigActionResolver::new( 12 | questioner.clone(), 13 | secrets.clone() 14 | ); 15 | (resolver, questioner, secrets) 16 | } 17 | 18 | pub struct TestPasswordQuestioner { 19 | is_called: Cell 20 | } 21 | impl TestPasswordQuestioner { 22 | pub fn new() -> Self { Self { is_called: Cell::new(false) } } 23 | pub fn was_asked(&self) -> bool { 24 | self.is_called.get() 25 | } 26 | } 27 | impl PasswordQuestioner for TestPasswordQuestioner { 28 | fn ask_password(&self, _username: &str) -> Result { 29 | self.is_called.set(true); 30 | Ok("asked_password".to_string()) 31 | } 32 | } 33 | 34 | pub struct TestSecrets{ 35 | password: RefCell 36 | } 37 | impl TestSecrets { 38 | pub fn new() -> Self { Self { password: RefCell::new("".to_string()) } } 39 | pub fn assert_check(&self, pwd: String) { assert_eq!(pwd, *self.password.borrow()); } 40 | } 41 | impl SecretsWriter for TestSecrets { 42 | fn write(&self, _key: &str, secret: &str) -> Result<(), WriteSecretError> { 43 | self.password.replace(secret.to_string()); 44 | Ok(()) 45 | } 46 | } -------------------------------------------------------------------------------- /src/commands/config/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod mocks; 2 | mod add_server_tests; 3 | mod update_server_tests; 4 | mod use_server_tests; 5 | mod show_tests; 6 | 7 | use self::mocks::create_resolver; 8 | pub use self::add_server_tests::*; 9 | pub use self::update_server_tests::*; 10 | pub use self::use_server_tests::*; 11 | pub use self::show_tests::*; 12 | 13 | use config::{ApplicationConfig, ElasticSearchServer, ElasticSearchServerType}; 14 | 15 | fn create_config() -> ApplicationConfig { 16 | ApplicationConfig { 17 | file_path: "".to_string(), 18 | default_server: None, 19 | servers: vec![ ] 20 | } 21 | } 22 | 23 | fn create_config_with_one_server() -> ApplicationConfig { 24 | ApplicationConfig { 25 | file_path: "".to_string(), 26 | default_server: None, 27 | servers: vec![ 28 | ElasticSearchServer { 29 | name: "test".to_string(), 30 | server: "address".to_string(), 31 | server_type: ElasticSearchServerType::Elastic, 32 | default_index: None, 33 | username: None 34 | } 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/config/tests/show_tests.rs: -------------------------------------------------------------------------------- 1 | use super::{create_config_with_one_server, create_resolver}; 2 | 3 | use commands::ConfigAction; 4 | 5 | #[test] 6 | fn should_do_nothing() { 7 | let config = create_config_with_one_server(); 8 | let (resolver, _, _) = create_resolver(); 9 | let new_config = resolver.resolve(ConfigAction::Show {}, config.clone()).unwrap(); 10 | assert_eq!(config, new_config); 11 | } -------------------------------------------------------------------------------- /src/commands/config/tests/update_server_tests.rs: -------------------------------------------------------------------------------- 1 | use super::{create_config, create_config_with_one_server, create_resolver}; 2 | 3 | use config::{ElasticSearchServerType}; 4 | use commands::config::resolver::{ConfigActionError}; 5 | 6 | use commands::ConfigAction; 7 | 8 | #[test] 9 | fn should_fails_on_updating_nonexistent_server() { 10 | let config = create_config(); 11 | let (resolver, _, _) = create_resolver(); 12 | 13 | let result = resolver.resolve(ConfigAction::UpdateServer { 14 | name: "test".to_string(), 15 | address: None, 16 | server_type: None, 17 | index: None, 18 | username: None, 19 | password: None, 20 | ask_password: false 21 | }, config); 22 | 23 | assert_eq!(Err(ConfigActionError::ServerDoesNotExists { server_name: "test".to_string() }), result); 24 | } 25 | 26 | #[test] 27 | fn should_not_change_config_if_no_options_provided() { 28 | let config = create_config_with_one_server(); 29 | let (resolver, _, _) = create_resolver(); 30 | 31 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 32 | name: "test".to_string(), 33 | address: None, 34 | server_type: None, 35 | index: None, 36 | username: None, 37 | password: None, 38 | ask_password: false 39 | }, config.clone()).unwrap(); 40 | 41 | assert_eq!(config, new_config); 42 | } 43 | 44 | #[test] 45 | fn should_update_address() { 46 | let mut config = create_config_with_one_server(); 47 | let (resolver, _, _) = create_resolver(); 48 | 49 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 50 | name: "test".to_string(), 51 | address: Some("updated_address".to_string()), 52 | server_type: None, 53 | index: None, 54 | username: None, 55 | password: None, 56 | ask_password: false 57 | }, config.clone()).unwrap(); 58 | 59 | config.servers.get_mut(0).unwrap().server = "updated_address".to_string(); 60 | 61 | assert_eq!(config, new_config); 62 | } 63 | 64 | 65 | #[test] 66 | fn should_update_server_type() { 67 | let mut config = create_config_with_one_server(); 68 | let (resolver, _, _) = create_resolver(); 69 | 70 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 71 | name: "test".to_string(), 72 | address: None, 73 | server_type: Some(ElasticSearchServerType::Kibana), 74 | index: None, 75 | username: None, 76 | password: None, 77 | ask_password: false 78 | }, config.clone()).unwrap(); 79 | 80 | config.servers.get_mut(0).unwrap().server_type = ElasticSearchServerType::Kibana; 81 | 82 | assert_eq!(config, new_config); 83 | } 84 | 85 | #[test] 86 | fn should_update_default_index() { 87 | let mut config = create_config_with_one_server(); 88 | let (resolver, _, _) = create_resolver(); 89 | 90 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 91 | name: "test".to_string(), 92 | address: None, 93 | server_type: None, 94 | index: Some("updated_index".to_string()), 95 | username: None, 96 | password: None, 97 | ask_password: false 98 | }, config.clone()).unwrap(); 99 | 100 | config.servers.get_mut(0).unwrap().default_index = Some("updated_index".to_string()); 101 | 102 | assert_eq!(config, new_config); 103 | } 104 | 105 | #[test] 106 | fn should_asks_for_password_if_username_is_present() { 107 | let mut config = create_config_with_one_server(); 108 | let (resolver, password, secrets) = create_resolver(); 109 | 110 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 111 | name: "test".to_string(), 112 | address: None, 113 | server_type: None, 114 | index: None, 115 | username: Some("updated_username".to_string()), 116 | password: None, 117 | ask_password: false 118 | }, config.clone()).unwrap(); 119 | 120 | config.servers.get_mut(0).unwrap().username = Some("updated_username".to_string()); 121 | 122 | assert!(password.was_asked()); 123 | assert_eq!(config, new_config); 124 | secrets.assert_check("asked_password".to_string()) 125 | } 126 | 127 | #[test] 128 | fn should_not_ask_for_password_if_password_is_provided() { 129 | let mut config = create_config_with_one_server(); 130 | let (resolver, password, secrets) = create_resolver(); 131 | 132 | let new_config = resolver.resolve(ConfigAction::UpdateServer { 133 | name: "test".to_string(), 134 | address: None, 135 | server_type: None, 136 | index: None, 137 | username: Some("updated_username".to_string()), 138 | password: Some("updated_password".to_string()), 139 | ask_password: false 140 | }, config.clone()).unwrap(); 141 | 142 | config.servers.get_mut(0).unwrap().username = Some("updated_username".to_string()); 143 | 144 | assert!(!password.was_asked()); 145 | assert_eq!(config, new_config); 146 | secrets.assert_check("updated_password".to_string()) 147 | } 148 | 149 | #[test] 150 | fn should_not_ask_for_password_if_ask_password_is_not_provided() { 151 | let config = create_config_with_one_server(); 152 | let (resolver, password, _) = create_resolver(); 153 | 154 | resolver.resolve(ConfigAction::UpdateServer { 155 | name: "test".to_string(), 156 | address: None, 157 | server_type: None, 158 | index: None, 159 | username: None, 160 | password: None, 161 | ask_password: false 162 | }, config.clone()).unwrap(); 163 | 164 | assert!(!password.was_asked()); 165 | } 166 | 167 | #[test] 168 | fn should_fails_if_password_provided_for_config_without_username() { 169 | let config = create_config_with_one_server(); 170 | let (resolver, _password, _secrets) = create_resolver(); 171 | 172 | let result = resolver.resolve(ConfigAction::UpdateServer { 173 | name: "test".to_string(), 174 | address: None, 175 | server_type: None, 176 | index: None, 177 | username: None, 178 | password: Some("updated_password".to_string()), 179 | ask_password: false 180 | }, config.clone()); 181 | 182 | assert_eq!(Err(ConfigActionError::UsernameShouldBeSpecified), result); 183 | } 184 | 185 | #[test] 186 | fn should_fails_if_password_ask_provided_for_config_without_username() { 187 | let config = create_config_with_one_server(); 188 | let (resolver, _password, _secrets) = create_resolver(); 189 | 190 | let result = resolver.resolve(ConfigAction::UpdateServer { 191 | name: "test".to_string(), 192 | address: None, 193 | server_type: None, 194 | index: None, 195 | username: None, 196 | password: None, 197 | ask_password: true 198 | }, config.clone()); 199 | 200 | assert_eq!(Err(ConfigActionError::UsernameShouldBeSpecified), result); 201 | } -------------------------------------------------------------------------------- /src/commands/config/tests/use_server_tests.rs: -------------------------------------------------------------------------------- 1 | use super::{create_config, create_config_with_one_server, create_resolver}; 2 | 3 | use commands::config::resolver::{ConfigActionError}; 4 | 5 | use commands::ConfigAction; 6 | 7 | #[test] 8 | fn should_fails_on_updating_nonexistent_server() { 9 | let config = create_config(); 10 | let (resolver, _, _) = create_resolver(); 11 | 12 | let result = resolver.resolve(ConfigAction::UpdateServer { 13 | name: "test".to_string(), 14 | address: None, 15 | server_type: None, 16 | index: None, 17 | username: None, 18 | password: None, 19 | ask_password: false 20 | }, config); 21 | 22 | assert_eq!(Err(ConfigActionError::ServerDoesNotExists { server_name: "test".to_string() }), result); 23 | } 24 | 25 | #[test] 26 | fn should_set_new_default_server() { 27 | let config = create_config_with_one_server(); 28 | let (resolver, _, _) = create_resolver(); 29 | 30 | let new_config = resolver.resolve(ConfigAction::UseServer { 31 | name: "test".to_string(), 32 | }, config).unwrap(); 33 | 34 | assert_eq!(Some("test".to_string()), new_config.default_server); 35 | } -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod search; 3 | mod config; 4 | 5 | pub use self::base::{Command}; 6 | pub use self::search::{SearchCommand}; 7 | pub use self::config::{ConfigCommand, ConfigAction}; -------------------------------------------------------------------------------- /src/commands/search.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ApplicationConfig, ElasticSearchServer, ElasticSearchServerType, GetServerError}; 2 | use crate::commands::Command; 3 | use crate::client::{Client, elastic::ElasticClient, kibana::KibanaProxyClient, stub::StubClient, SearchRequest}; 4 | use crate::display::*; 5 | 6 | use clap::ArgMatches; 7 | 8 | use std::string::ToString; 9 | use std::sync::Arc; 10 | use error::ApplicationError; 11 | use config::SecretsReader; 12 | 13 | pub struct SearchCommand { 14 | pub client: Box, 15 | pub renderer: Box, 16 | pub request: SearchRequest, 17 | pub secrets: Arc 18 | } 19 | 20 | impl Command for SearchCommand { 21 | fn execute(&mut self) -> Result<(), ApplicationError> { 22 | info!("Executing search '{}' on index '{}'", self.request.query, self.request.index); 23 | 24 | let _ = self.client.execute(&self.request).map_err(|err| { 25 | error!("Cannot fetch items from server: {}", err) 26 | }).map(|collector| { 27 | self.renderer.render(collector); 28 | }); 29 | 30 | Ok(()) 31 | } 32 | } 33 | 34 | impl SearchCommand { 35 | pub fn parse(config: &ApplicationConfig, secrets: Arc, matches: &ArgMatches, sub_match: &ArgMatches) -> Result { 36 | let server = match config.get_server(matches.value_of("server")) { 37 | Ok(server) => Ok(server), 38 | Err(GetServerError::ServerNotFound { server }) => { 39 | error!("Server with name '{}' not found", server); 40 | Err(ApplicationError) 41 | } 42 | Err(GetServerError::ServerNotSpecified) => { 43 | error!("The server is not specified."); 44 | error!("Hint: use 'elastic-cli config use server '"); 45 | error!("Hint: use option --server, e.g. 'elastic-cli --server search ...'"); 46 | Err(ApplicationError) 47 | } 48 | Err(GetServerError::NoConfiguredServers) => { 49 | error!("There are no servers in the config file"); 50 | error!("Hint: use 'elastic-cli config add server --address
'"); 51 | Err(ApplicationError) 52 | } 53 | }?; 54 | 55 | let pager_enabled = sub_match.is_present("pager"); 56 | 57 | let _size = sub_match.value_of("size").map(str::parse).unwrap_or(Ok(1000)) 58 | .map_err(|err| { 59 | error!("Argument 'size' has invalid value: {}", err); 60 | ApplicationError 61 | })?; 62 | 63 | let buffer_size = sub_match.value_of("buffer").map(str::parse).unwrap_or(Ok(1000)) 64 | .map_err(|err| { 65 | error!("Argument 'buffer' has invalid value: {}", err); 66 | ApplicationError 67 | })?; 68 | 69 | let query = sub_match.value_of("query") 70 | .map(ToString::to_string) 71 | .ok_or_else(|| { 72 | error!("Query must be specified"); 73 | ApplicationError 74 | })?; 75 | 76 | let index = sub_match.value_of("index") 77 | .map(ToString::to_string) 78 | .or_else(|| server.default_index.clone()) 79 | .unwrap_or_else(|| "*".to_string()); 80 | 81 | let format = sub_match.value_of("output") 82 | .map(|f| match f { 83 | "pretty" => OutputFormat::Pretty, 84 | "json" => OutputFormat::JSON, 85 | custom => OutputFormat::Custom(custom.to_string()) 86 | }).unwrap_or(OutputFormat::Pretty); 87 | 88 | let extractor = sub_match.value_of("fields") 89 | .map(|s| JSONExtractor::filtered(s.split(','))) 90 | .unwrap_or_else(JSONExtractor::default); 91 | 92 | let renderer = Self::create_renderer(pager_enabled, format, extractor); 93 | let client = Self::create_client(secrets.clone(), server, buffer_size); 94 | 95 | Ok(SearchCommand { 96 | client, 97 | request: SearchRequest { query, index }, 98 | renderer, 99 | secrets 100 | }) 101 | } 102 | 103 | fn create_renderer( 104 | pager_enabled: bool, 105 | format: OutputFormat, 106 | extractor: JSONExtractor 107 | ) -> Box { 108 | if pager_enabled { 109 | Box::new(PagedRenderer::new(format, extractor)) 110 | } else { 111 | Box::new(SimpleRenderer::new(format, extractor)) 112 | } 113 | } 114 | 115 | fn create_client( 116 | secrets: Arc, 117 | server: &ElasticSearchServer, 118 | buffer_size: usize 119 | ) -> Box { 120 | match server.server_type { 121 | ElasticSearchServerType::Elastic => Box::new(ElasticClient::create(secrets, server.clone(), buffer_size)), 122 | ElasticSearchServerType::Kibana => Box::new(KibanaProxyClient::create(secrets, server.clone(), buffer_size)), 123 | ElasticSearchServerType::Stub => Box::new(StubClient::new(buffer_size)) 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/config/conf.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | 3 | use std::fs::{File, OpenOptions}; 4 | use std::vec::Vec; 5 | use std::path::{Path, PathBuf}; 6 | use std::io::Write; 7 | use serde_yaml; 8 | use dirs; 9 | 10 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 11 | pub struct ApplicationConfig { 12 | #[serde(skip_deserializing, skip_serializing)] 13 | pub file_path: String, 14 | 15 | pub default_server: Option, 16 | pub servers: Vec, 17 | } 18 | 19 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 20 | pub struct ElasticSearchServer { 21 | pub name: String, 22 | pub server: String, 23 | #[serde(default)] 24 | pub server_type: config::ElasticSearchServerType, 25 | pub default_index: Option, 26 | pub username: Option 27 | } 28 | 29 | impl ApplicationConfig { 30 | pub fn load_default() -> Result { 31 | dirs::home_dir() 32 | .ok_or(config::Error::CannotFindHomeDirectory()) 33 | .map(|home_dir| home_dir.join(PathBuf::from(".elastic-cli")).into_os_string()) 34 | .and_then(|os_str| os_str.into_string().map_err(|_| config::Error::CannotFindHomeDirectory())) 35 | .and_then(|path| ApplicationConfig::load_file_or_create(&path)) 36 | } 37 | 38 | fn load_file_or_create(path: &str) -> Result { 39 | if !Path::new(path).exists() { 40 | warn!("File {} does not exist, creating...", path); 41 | let config = ApplicationConfig { 42 | file_path: path.to_owned(), 43 | default_server: None, 44 | servers: vec![] 45 | }; 46 | return config.save_file().map(|_| config); 47 | } 48 | ApplicationConfig::load_file(path) 49 | } 50 | 51 | pub fn load_file(path: &str) -> Result { 52 | info!("Loading config from: {}", path); 53 | 54 | File::open(path) 55 | .map_err(|err| { 56 | error!("Cannot open file {}: {}", path, err); 57 | From::from(err) 58 | }) 59 | .and_then(|file| serde_yaml::from_reader::(file).map_err(From::from)) 60 | .map(|mut config| { 61 | config.file_path = path.to_owned(); 62 | config 63 | }) 64 | } 65 | 66 | pub fn get_server(&self, name: Option) -> Result<&ElasticSearchServer, config::GetServerError> where S: Into { 67 | if self.servers.is_empty() { 68 | return Err(config::GetServerError::NoConfiguredServers); 69 | } 70 | 71 | let server_name = match (name, &self.default_server) { 72 | (Some(s_name), _) => s_name.into(), 73 | (None, &Some(ref s_name)) => s_name.to_owned(), 74 | _ => return Err(config::GetServerError::ServerNotSpecified) 75 | }; 76 | 77 | self.servers.iter().find(|server| server.name == server_name) 78 | .ok_or_else(||config::GetServerError::ServerNotFound { server: server_name.clone() }) 79 | } 80 | 81 | pub fn save_file(&self) -> Result<(), config::Error> { 82 | let mut file = self.open_file_or_create()?; 83 | let yaml = serde_yaml::to_string(self).map_err(|err| { 84 | error!("Cannot serialize configuration: {}", err); 85 | config::Error::YamlError { inner: err } 86 | })?; 87 | file.write_all(yaml.as_bytes()).map_err(|err| { 88 | error!("Cannot write configuration file: {}", err); 89 | From::from(err) 90 | }) 91 | } 92 | 93 | fn open_file_or_create(&self) -> Result { 94 | if Path::new(&self.file_path).exists() { 95 | OpenOptions::new().write(true).open(&self.file_path).map_err(|err| { 96 | error!("Cannot open configuration file {}: {}", self.file_path, err); 97 | config::Error::IOError { inner: err } 98 | }) 99 | } else { 100 | File::create(&self.file_path).map_err(|err| { 101 | error!("Cannot create empty configuration file {}: {}", self.file_path, err); 102 | config::Error::IOError { inner: err } 103 | }) 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/config/error.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_yaml; 2 | 3 | #[derive(Debug, Fail)] 4 | pub enum GetServerError { 5 | #[fail(display = "server {} not found", server)] 6 | ServerNotFound { server: String }, 7 | #[fail(display = "no servers configured")] 8 | NoConfiguredServers, 9 | #[fail(display = "server not specified")] 10 | ServerNotSpecified 11 | } 12 | 13 | #[derive(Debug, Fail)] 14 | pub enum Error { 15 | #[fail(display = "cannot deserialize yaml: {}", inner)] 16 | YamlError { inner: serde_yaml::Error }, 17 | #[fail(display = "cannot read file: {}", inner)] 18 | IOError { inner: std::io::Error }, 19 | #[fail(display = "cannot find config file: {}", inner)] 20 | PathError { inner: std::env::JoinPathsError }, 21 | #[fail(display = "cannot find home directory")] 22 | CannotFindHomeDirectory() 23 | } 24 | 25 | impl From<::std::env::JoinPathsError> for Error { 26 | fn from(error: ::std::env::JoinPathsError) -> Self { 27 | Error::PathError { inner: error } 28 | } 29 | } 30 | 31 | impl From<::std::io::Error> for Error { 32 | fn from(error: ::std::io::Error) -> Self { 33 | Error::IOError { inner: error } 34 | } 35 | } 36 | 37 | impl From<::serde_yaml::Error> for Error { 38 | fn from(error: ::serde_yaml::Error) -> Self { 39 | Error::YamlError { inner: error } 40 | } 41 | } -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod conf; 2 | mod error; 3 | mod server_type; 4 | mod secrets; 5 | 6 | pub use self::conf::{ApplicationConfig, ElasticSearchServer}; 7 | pub use self::error::{Error, GetServerError}; 8 | pub use self::server_type::ElasticSearchServerType; 9 | pub use self::secrets::*; -------------------------------------------------------------------------------- /src/config/secrets.rs: -------------------------------------------------------------------------------- 1 | use keyring::{Keyring, KeyringError}; 2 | 3 | pub trait SecretsReader { 4 | fn read(&self, key: &str) -> Result, ReadSecretError>; 5 | 6 | fn get_credentials(&self, username: &str) -> Result, ReadSecretError>; 7 | } 8 | 9 | pub trait SecretsWriter { 10 | fn write(&self, key: &str, secret: &str) -> Result<(), WriteSecretError>; 11 | } 12 | 13 | pub struct SystemSecretsStorage { 14 | service: String 15 | } 16 | 17 | #[derive(Debug, Fail)] 18 | #[fail(display = "cannot read secret by key {}: {}", key, inner)] 19 | pub struct ReadSecretError { 20 | key: String, 21 | inner: String 22 | } 23 | 24 | #[derive(Debug, Fail, PartialEq)] 25 | #[fail(display = "cannot save secret by key {}: {}", key, inner)] 26 | pub struct WriteSecretError { 27 | key: String, 28 | inner: String 29 | } 30 | 31 | #[derive(Clone)] 32 | pub struct Credentials { 33 | pub username: String, 34 | pub password: String 35 | } 36 | 37 | impl SystemSecretsStorage { 38 | pub fn new(service: impl Into) -> Self { 39 | Self { service: service.into() } 40 | } 41 | } 42 | 43 | impl SecretsReader for SystemSecretsStorage { 44 | fn read(&self, key: &str) -> Result, ReadSecretError> { 45 | let secret = Keyring::new(&self.service, key) 46 | .get_password(); 47 | match secret { 48 | Ok(secret) => Ok(Some(secret)), 49 | Err(KeyringError::NoPasswordFound) => Ok(None), 50 | Err(err) => Err(ReadSecretError { key: key.to_string(), inner: format!("{}", err) }) 51 | } 52 | } 53 | 54 | fn get_credentials(&self, username: &str) -> Result, ReadSecretError> { 55 | self.read(username) 56 | .map(|password| { 57 | password.map(|password| Credentials { username: username.to_string(), password }) 58 | }) 59 | } 60 | } 61 | 62 | impl SecretsWriter for SystemSecretsStorage { 63 | fn write(&self, key: &str, secret: &str) -> Result<(), WriteSecretError> { 64 | Keyring::new(&self.service, &key) 65 | .set_password(secret) 66 | .map_err(|err| { 67 | WriteSecretError { key: key.to_string(), inner: format!("{}", err) } 68 | }) 69 | } 70 | } -------------------------------------------------------------------------------- /src/config/server_type.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::{Serialize, Serializer}; 2 | use serde::de::{self, Deserialize, Deserializer, Visitor}; 3 | 4 | use std::fmt; 5 | use std::str::FromStr; 6 | 7 | #[derive(Debug, PartialEq, Clone)] 8 | pub enum ElasticSearchServerType { 9 | Elastic, 10 | Kibana, 11 | Stub 12 | } 13 | 14 | impl Default for ElasticSearchServerType { 15 | fn default() -> Self { 16 | ElasticSearchServerType::Elastic 17 | } 18 | } 19 | 20 | struct ElasticSearchServerTypeVisitor; 21 | 22 | impl Serialize for ElasticSearchServerType { 23 | fn serialize(&self, s: S) -> Result where S: Serializer { 24 | match self { 25 | ElasticSearchServerType::Elastic => s.serialize_str("elastic"), 26 | ElasticSearchServerType::Kibana => s.serialize_str("kibana"), 27 | ElasticSearchServerType::Stub => s.serialize_str("stub"), 28 | } 29 | } 30 | } 31 | 32 | impl<'de> Deserialize<'de> for ElasticSearchServerType { 33 | fn deserialize(d: D) -> Result where D: Deserializer<'de> { 34 | d.deserialize_str(ElasticSearchServerTypeVisitor) 35 | } 36 | } 37 | 38 | impl<'de> Visitor<'de> for ElasticSearchServerTypeVisitor { 39 | type Value = ElasticSearchServerType; 40 | 41 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 42 | formatter.write_str("'elastic' or 'kibana'") 43 | } 44 | 45 | fn visit_str(self, v: &str) -> Result where E: de::Error { 46 | ElasticSearchServerType::from_str(v) 47 | .map_err(|err| E::custom(format!("{}", err)) ) 48 | } 49 | } 50 | 51 | #[derive(Debug, Fail)] 52 | #[fail(display = "unknown server type: {}", value)] 53 | pub struct UnknownElasticSearchServerTypeError { 54 | value: String 55 | } 56 | 57 | impl FromStr for ElasticSearchServerType { 58 | type Err = UnknownElasticSearchServerTypeError; 59 | 60 | fn from_str(s: &str) -> Result { 61 | match s { 62 | "elastic" => Ok(ElasticSearchServerType::Elastic), 63 | "kibana" => Ok(ElasticSearchServerType::Kibana), 64 | "stub" => Ok(ElasticSearchServerType::Stub), 65 | value => Err(UnknownElasticSearchServerTypeError { value: value.to_string() }) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/display/extractor.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::iter::FromIterator; 3 | use std::convert::Into; 4 | use std::collections::HashSet; 5 | use std::collections::btree_map::BTreeMap; 6 | 7 | pub struct JSONExtractor { 8 | field_delimiter: String, 9 | fields: Option> 10 | } 11 | 12 | impl JSONExtractor { 13 | pub fn default() -> Self { 14 | JSONExtractor { 15 | field_delimiter: ".".to_string(), 16 | fields: None 17 | } 18 | } 19 | 20 | pub fn filtered, I: IntoIterator>(fields: I) -> Self { 21 | JSONExtractor { 22 | field_delimiter: ".".to_string(), 23 | fields: Some(HashSet::from_iter(fields.into_iter().map(Into::into))) 24 | } 25 | } 26 | 27 | pub fn extract(&self, item: &Value) -> BTreeMap { 28 | let mut map = BTreeMap::new(); 29 | self.extract_one(&[], item, &mut map); 30 | map 31 | } 32 | 33 | fn extract_one(&self, path: &[String], hit: &Value, map: &mut BTreeMap) { 34 | match hit { 35 | &Value::Object(ref object) => { 36 | for (key, value) in object { 37 | let mut new_path = path.to_owned(); 38 | new_path.push(key.clone()); 39 | self.extract_one(&new_path, &value, map); 40 | } 41 | } 42 | &Value::Array(ref array) => { 43 | for (index, value) in array.iter().enumerate() { 44 | let mut new_path = path.to_owned(); 45 | new_path.push(index.to_string()); 46 | self.extract_one(&new_path, &value, map); 47 | } 48 | } 49 | primitive => { 50 | let key = path.join(&self.field_delimiter); 51 | if self.is_field_ok(&key) { 52 | map.insert(key, self.prepare_primitive(primitive)); 53 | } 54 | } 55 | } 56 | } 57 | 58 | fn is_field_ok(&self, field: &str) -> bool { 59 | self.fields 60 | .as_ref() 61 | .map(|f| f.contains(field)) 62 | .unwrap_or(true) 63 | } 64 | 65 | fn prepare_primitive(&self, value: &serde_json::Value) -> String { 66 | match value { 67 | &serde_json::Value::String(ref str_value) => str_value.to_string(), 68 | primitive => primitive.to_string() 69 | } 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::JSONExtractor; 76 | 77 | fn get_value() -> serde_json::Value { 78 | json!({ 79 | "root": { 80 | "obj": { 81 | "strKey": "str1", 82 | "intKey": 1 83 | }, 84 | "arr": [ 85 | { "value": 1 }, 86 | { "value": 2 }, 87 | { "value": 3 } 88 | ] 89 | } 90 | }) 91 | } 92 | 93 | #[test] 94 | fn it_should_extract_values_from_json() { 95 | let map = JSONExtractor::default().extract(&get_value()); 96 | assert_eq!(Some("str1".to_string()).as_ref(), map.get("root.obj.strKey")); 97 | assert_eq!(Some("1".to_string()).as_ref(), map.get("root.obj.intKey")); 98 | assert_eq!(Some("1".to_string()).as_ref(), map.get("root.arr.0.value")); 99 | assert_eq!(Some("2".to_string()).as_ref(), map.get("root.arr.1.value")); 100 | assert_eq!(Some("3".to_string()).as_ref(), map.get("root.arr.2.value")); 101 | 102 | assert_eq!(None, map.get("root")); 103 | assert_eq!(None, map.get("root.obj")); 104 | assert_eq!(None, map.get("root.obj.anotherKey")); 105 | assert_eq!(None, map.get("root.arr")); 106 | assert_eq!(None, map.get("root.arr.3.value")); 107 | } 108 | 109 | #[test] 110 | fn it_should_filter_fields() { 111 | let fields = vec!["root.arr.2.value", "root.obj.strKey"]; 112 | let map = JSONExtractor::filtered(fields) 113 | .extract(&get_value()); 114 | 115 | assert_eq!(Some("str1".to_string()).as_ref(), map.get("root.obj.strKey")); 116 | assert_eq!(Some("3".to_string()).as_ref(), map.get("root.arr.2.value")); 117 | 118 | assert_eq!(None, map.get("root.obj.intKey")); 119 | assert_eq!(None, map.get("root.arr.0.value")); 120 | assert_eq!(None, map.get("root.arr.1.value")); 121 | assert_eq!(None, map.get("root")); 122 | assert_eq!(None, map.get("root.obj")); 123 | assert_eq!(None, map.get("root.obj.anotherKey")); 124 | assert_eq!(None, map.get("root.arr")); 125 | assert_eq!(None, map.get("root.arr.3.value")); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/display/format.rs: -------------------------------------------------------------------------------- 1 | use super::{JSONExtractor}; 2 | 3 | use serde_json::Value; 4 | use strfmt::strfmt; 5 | use colored::*; 6 | 7 | use std::fmt::Write; 8 | use std::collections::HashMap; 9 | 10 | pub enum OutputFormat { 11 | JSON, 12 | Pretty, 13 | Custom(String) 14 | } 15 | 16 | 17 | pub struct Formatter { 18 | format: OutputFormat, 19 | extractor: JSONExtractor 20 | } 21 | 22 | impl Formatter { 23 | pub fn new(format: OutputFormat, extractor: JSONExtractor) -> Self { 24 | Self { 25 | format, extractor 26 | } 27 | } 28 | 29 | pub fn format(&self, item: &Value, index: usize) -> String { 30 | let mut str = String::new(); 31 | match &self.format { 32 | OutputFormat::Pretty => { 33 | if index > 0 { 34 | let _ = writeln!(str, "{}", "-".repeat(4).blue().bold()); 35 | } 36 | let map = self.extractor.extract(item); 37 | for (key, value) in map { 38 | let _ = writeln!(str, "{}: {}", key.green().bold(), format_string(&value)); 39 | } 40 | }, 41 | OutputFormat::JSON => { 42 | let _ = writeln!(str, "{}", item); 43 | }, 44 | OutputFormat::Custom(format) => { 45 | let map: &HashMap = &self.extractor.extract(item).into_iter().collect(); 46 | let _ = writeln!(str, "{}", strfmt(format, map).unwrap_or_else(|_| "Cannot format item".to_owned())); 47 | } 48 | }; 49 | str 50 | } 51 | } 52 | 53 | // TODO refactor this 54 | fn format_string(value: &str) -> String { 55 | Some(value) 56 | .map(|s| str::replace(s, "\\n", "\n")) 57 | .map(|s| str::replace(&s, "\\t", "\t")) 58 | .unwrap() 59 | } 60 | 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::{ JSONExtractor, format_string }; 65 | use display::format::{ Formatter, OutputFormat }; 66 | use colored::*; 67 | 68 | #[test] 69 | fn renderer_pretty_format_should_render_first_row_without_delimiter() { 70 | let str = Formatter::new(OutputFormat::Pretty, JSONExtractor::default()) 71 | .format( 72 | &json!({ 73 | "root": { 74 | "obj": { 75 | "strKey": "str1" 76 | }, 77 | } 78 | }), 79 | 0); 80 | 81 | assert_eq!(format!("{}: {}\n", "root.obj.strKey".green().bold(), "str1"), str); 82 | } 83 | 84 | #[test] 85 | fn renderer_pretty_format_should_render_not_first_row_with_delimiter() { 86 | let str = Formatter::new(OutputFormat::Pretty, JSONExtractor::default()) 87 | .format( 88 | &json!({ 89 | "root": { 90 | "obj": { 91 | "strKey": "str1" 92 | }, 93 | } 94 | }), 95 | 5); 96 | 97 | assert_eq!(format!("{}\n{}: {}\n", "----".blue().bold(), "root.obj.strKey".green().bold(), "str1"), str); 98 | } 99 | 100 | #[test] 101 | fn renderer_pretty_format_should_render_several_fields() { 102 | let str = Formatter::new(OutputFormat::Pretty, JSONExtractor::default()) 103 | .format( 104 | &json!({ 105 | "root": { 106 | "obj": { 107 | "strKey": "str1" 108 | }, 109 | "arr": [ 110 | { "value": 1 } 111 | ] 112 | } 113 | }), 114 | 0); 115 | 116 | assert_eq!( 117 | format!( 118 | "{}: {}\n{}: {}\n", 119 | "root.arr.0.value".green().bold(), 120 | "1", 121 | "root.obj.strKey".green().bold(), 122 | "str1" 123 | ), 124 | str 125 | ); 126 | } 127 | 128 | #[test] 129 | fn format_string_should_replaces_new_line_and_tab_placeholders_to_real_symbols() { 130 | assert_eq!( 131 | "abc\tabc\nabc - abc\tabc\nabc", 132 | format_string("abc\\tabc\\nabc - abc\\tabc\\nabc") 133 | ) 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | mod extractor; 2 | mod renderer; 3 | mod format; 4 | mod pager; 5 | 6 | pub use self::extractor::*; 7 | pub use self::renderer::*; 8 | pub use self::format::*; 9 | pub use self::pager::*; 10 | 11 | -------------------------------------------------------------------------------- /src/display/pager/collector.rs: -------------------------------------------------------------------------------- 1 | use crate::display::format::Formatter; 2 | use crate::utils::{OptionalSkip, OptionalTake}; 3 | 4 | use serde_json::Value; 5 | 6 | use std::cmp::max; 7 | use std::sync::Arc; 8 | 9 | pub struct LinesCollector { 10 | formatter: Arc, 11 | 12 | skip_items: Option, 13 | take_items: Option, 14 | take_lines: Option, 15 | take_last_lines: Option 16 | } 17 | 18 | pub struct CollectedLines { 19 | pub lines: Vec, 20 | pub items_count: usize, 21 | pub has_cropped_items: bool 22 | } 23 | 24 | impl LinesCollector { 25 | pub fn new( 26 | formatter: Arc, 27 | ) -> Self { 28 | Self { 29 | formatter, 30 | skip_items: None, 31 | take_items: None, 32 | take_lines: None, 33 | take_last_lines: None 34 | } 35 | } 36 | 37 | pub fn skip_items(mut self, n: usize) -> Self { 38 | self.skip_items = Some(n); 39 | self 40 | } 41 | 42 | pub fn take_items(mut self, n: usize) -> Self { 43 | self.take_items = Some(n); 44 | self 45 | } 46 | 47 | pub fn take_lines(mut self, n: usize) -> Self { 48 | self.take_lines = Some(n); 49 | self 50 | } 51 | 52 | pub fn take_last_lines(mut self, n: usize) -> Self { 53 | self.take_last_lines = Some(n); 54 | self 55 | } 56 | 57 | pub fn collect(self, source: impl Iterator) -> CollectedLines { 58 | let mut real_lines_count: usize = 0; 59 | let mut items_count: usize = 0; 60 | let mut items_sizes: Vec = vec![]; 61 | 62 | let mut lines: Vec = source 63 | .skip_by_option(self.skip_items) 64 | .take_by_option(self.take_items) 65 | .enumerate() 66 | .map(|(index, item)| self.formatter.format(&item, index)) 67 | .map(|text| { 68 | text.lines() 69 | .map(ToString::to_string) 70 | .collect::>() 71 | }) 72 | .filter(|item_lines| { 73 | items_count += 1; 74 | real_lines_count += item_lines.len(); 75 | items_sizes.push(item_lines.len()); 76 | true 77 | }) 78 | .flat_map(|item_lines| item_lines) 79 | .take_by_option(self.take_lines) 80 | .collect(); 81 | 82 | if let Some(take_last_lines) = self.take_last_lines { 83 | let lines_to_skip = max(lines.len(), take_last_lines) - take_last_lines; 84 | let mut total = 0; 85 | items_count = items_sizes.iter() 86 | .skip_while(|c| { 87 | total += *c; 88 | real_lines_count -= *c; 89 | total < lines_to_skip 90 | }).count(); 91 | lines.drain(..lines_to_skip); 92 | } 93 | 94 | let lines_count = lines.len(); 95 | 96 | CollectedLines { 97 | lines, 98 | items_count, 99 | has_cropped_items: real_lines_count != lines_count 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/display/pager/mod.rs: -------------------------------------------------------------------------------- 1 | mod ui; 2 | mod scroll_mode; 3 | mod collector; 4 | 5 | pub use self::ui::*; 6 | pub use self::scroll_mode::*; 7 | pub use self::collector::*; 8 | -------------------------------------------------------------------------------- /src/display/pager/scroll_mode.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq)] 2 | pub enum ScrollMode { 3 | ScrollUp, 4 | ScrollDown 5 | } 6 | -------------------------------------------------------------------------------- /src/display/pager/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Collector; 2 | use crate::display::Formatter; 3 | use super::ScrollMode; 4 | 5 | use serde_json::Value; 6 | use termion::event::{Key, Event, MouseEvent, MouseButton}; 7 | use termion::input::TermRead; 8 | use termion::raw::{IntoRawMode, RawTerminal}; 9 | use termion::screen::AlternateScreen; 10 | 11 | use std::cmp::max; 12 | use std::io::{Write, stdout, stdin, Stdout}; 13 | use std::sync::Arc; 14 | use display::pager::collector::{LinesCollector, CollectedLines}; 15 | 16 | pub struct Pager { 17 | formatter: Arc, 18 | collector: Collector, 19 | stdout: AlternateScreen>, 20 | top_index: usize, 21 | bottom_index: usize, 22 | scroll_mode: ScrollMode, 23 | has_cropped_item: bool 24 | } 25 | 26 | impl Pager { 27 | pub fn new(collector: Collector, formatter: Arc) -> Self { 28 | Pager { 29 | formatter: formatter.clone(), 30 | collector, 31 | stdout: AlternateScreen::from(stdout().into_raw_mode().unwrap()), 32 | top_index: 0, 33 | bottom_index: 0, 34 | scroll_mode: ScrollMode::ScrollUp, 35 | has_cropped_item: false 36 | } 37 | } 38 | 39 | pub fn start(mut self) { 40 | let stdin = stdin(); 41 | 42 | self.display(); 43 | for c in stdin.events() { 44 | match c.unwrap() { 45 | Event::Key(Key::Char('q')) | 46 | Event::Key(Key::Ctrl('c')) => break, 47 | Event::Key(Key::Up) | 48 | Event::Key(Key::PageUp) | 49 | Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => { 50 | if (self.scroll_mode == ScrollMode::ScrollUp) || !self.has_cropped_item { 51 | self.top_index = max(self.top_index, 1) - 1; 52 | } 53 | self.scroll_mode = ScrollMode::ScrollUp; 54 | }, 55 | Event::Key(Key::Down) | 56 | Event::Key(Key::PageDown) | 57 | Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => { 58 | if (self.scroll_mode == ScrollMode::ScrollDown) || !self.has_cropped_item { 59 | self.bottom_index += 1; 60 | } 61 | self.scroll_mode = ScrollMode::ScrollDown; 62 | }, 63 | _ => {} 64 | } 65 | self.display(); 66 | } 67 | } 68 | 69 | fn display(&mut self) { 70 | let (_, height) = termion::terminal_size().unwrap(); 71 | let working_height = (height - 2) as usize; 72 | 73 | let lines = self.get_lines(working_height); 74 | 75 | self.has_cropped_item = lines.has_cropped_items; 76 | match self.scroll_mode { 77 | ScrollMode::ScrollUp => { 78 | self.bottom_index = self.top_index + lines.items_count; 79 | }, 80 | ScrollMode::ScrollDown => { 81 | self.top_index = self.bottom_index - lines.items_count; 82 | } 83 | } 84 | 85 | self.clear(); 86 | self.print_lines(lines.lines); 87 | 88 | write!(self.stdout, "{}Loaded {} from {}, displayed {}-{} ({} items)", 89 | termion::cursor::Goto(1, height - 1), 90 | self.collector.from, 91 | self.collector.total, 92 | self.top_index, 93 | self.bottom_index, 94 | lines.items_count 95 | ).unwrap(); 96 | 97 | write!(self.stdout, "{}Press q to exit, ↑/↓ to navigate", 98 | termion::cursor::Goto(1, height) 99 | ).unwrap(); 100 | 101 | self.stdout.flush().unwrap(); 102 | } 103 | 104 | fn clear(&mut self) { 105 | write!(self.stdout, 106 | "{}{}{}", 107 | termion::clear::All, 108 | termion::cursor::Goto(1, 1), 109 | termion::cursor::Hide 110 | ).unwrap(); 111 | } 112 | 113 | fn get_lines(&mut self, limit: usize) -> CollectedLines { 114 | match self.scroll_mode { 115 | ScrollMode::ScrollUp => { 116 | LinesCollector::new(self.formatter.clone()) 117 | .skip_items(self.top_index) 118 | .take_lines(limit) 119 | }, 120 | ScrollMode::ScrollDown => { 121 | LinesCollector::new(self.formatter.clone()) 122 | .take_items(self.bottom_index) 123 | .take_last_lines(limit) 124 | } 125 | }.collect(self.collector.iter()) 126 | } 127 | 128 | fn print_lines(&mut self, lines: Vec) { 129 | lines.iter().enumerate().for_each(|(index, line)| { 130 | write!(self.stdout, "{}{}", 131 | termion::cursor::Goto(1, (index + 1) as u16), 132 | line 133 | ).unwrap(); 134 | }); 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /src/display/renderer.rs: -------------------------------------------------------------------------------- 1 | use super::{OutputFormat, JSONExtractor, Pager}; 2 | use crate::client::Collector; 3 | use crate::display::{Formatter}; 4 | 5 | use serde_json::Value; 6 | 7 | use std::sync::Arc; 8 | 9 | pub trait Renderer { 10 | fn render(&mut self, collector: Collector); 11 | } 12 | 13 | pub struct SimpleRenderer { 14 | formatter: Formatter 15 | } 16 | 17 | impl SimpleRenderer { 18 | pub fn new(format: OutputFormat, extractor: JSONExtractor) -> Self { 19 | Self { 20 | formatter: Formatter::new(format, extractor) 21 | } 22 | } 23 | } 24 | 25 | impl Renderer for SimpleRenderer { 26 | fn render(&mut self, mut collector: Collector) { 27 | collector.iter().enumerate() 28 | .for_each(|(index, item)| { 29 | print!("{}", self.formatter.format(&item, index)); 30 | }); 31 | } 32 | } 33 | 34 | pub struct PagedRenderer { 35 | formatter: Arc 36 | } 37 | 38 | impl PagedRenderer { 39 | pub fn new(format: OutputFormat, extractor: JSONExtractor) -> Self { 40 | Self { 41 | formatter: Arc::new(Formatter::new(format, extractor)) 42 | } 43 | } 44 | } 45 | 46 | impl Renderer for PagedRenderer { 47 | fn render(&mut self, collector: Collector) { 48 | Pager::new( 49 | collector, 50 | self.formatter.clone() 51 | ).start() 52 | } 53 | } -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Fail)] 2 | #[fail(display = "An error occurred.")] 3 | pub struct ApplicationError; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate elastic; 4 | extern crate serde; 5 | #[macro_use] 6 | extern crate serde_derive; 7 | extern crate serde_yaml; 8 | #[macro_use] 9 | extern crate serde_json; 10 | extern crate stderrlog; 11 | #[macro_use] 12 | extern crate log; 13 | extern crate reqwest; 14 | extern crate colored; 15 | extern crate strfmt; 16 | extern crate dirs; 17 | extern crate base64; 18 | #[macro_use] 19 | extern crate failure; 20 | extern crate keyring; 21 | extern crate rpassword; 22 | extern crate termion; 23 | extern crate core; 24 | 25 | mod config; 26 | mod commands; 27 | mod error; 28 | mod client; 29 | mod display; 30 | mod utils; 31 | 32 | use clap::{App, ArgMatches}; 33 | use config::{ApplicationConfig, SystemSecretsStorage}; 34 | use commands::{Command}; 35 | use error::ApplicationError; 36 | 37 | use std::sync::Arc; 38 | 39 | fn main() { 40 | if run_application().is_err() { 41 | std::process::exit(1); 42 | } 43 | } 44 | 45 | fn run_application() -> Result<(), ApplicationError> { 46 | let yaml = load_yaml!("app.yaml"); 47 | let app = App::from_yaml(yaml) 48 | .name(env!("CARGO_PKG_NAME")) 49 | .author(env!("CARGO_PKG_AUTHORS")) 50 | .about(env!("CARGO_PKG_DESCRIPTION")) 51 | .version(env!("CARGO_PKG_VERSION")); 52 | let args = app.get_matches(); 53 | 54 | configure_logger(&args)?; 55 | 56 | let secrets = Arc::new(SystemSecretsStorage::new("elastic-cli")); 57 | 58 | let config = args.value_of("config") 59 | .map_or_else(ApplicationConfig::load_default, ApplicationConfig::load_file) 60 | .map_err(|err| { 61 | error!("Cannot read configuration: {}", err); 62 | ApplicationError 63 | })?; 64 | 65 | match args.subcommand() { 66 | ("search", Some(sub_match)) => commands::SearchCommand::parse(&config, secrets, &args, sub_match)?.execute(), 67 | ("config", Some(sub_match)) => commands::ConfigCommand::parse(config, secrets, sub_match)?.execute(), 68 | _ => { 69 | println!("{}", args.usage()); 70 | Err(ApplicationError) 71 | } 72 | } 73 | } 74 | 75 | fn configure_logger(args: &ArgMatches) -> Result<(), ApplicationError> { 76 | let verbose = args.occurrences_of("verbosity") as usize; 77 | let quiet = args.is_present("quiet"); 78 | stderrlog::new() 79 | .module(module_path!()) 80 | .quiet(quiet) 81 | .verbosity(verbose + 1) 82 | .init() 83 | .map_err(|err| { 84 | error!("Cannot configure logger: {}", err); 85 | ApplicationError 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod skip_by_option; 2 | mod take_by_option; 3 | 4 | pub use self::skip_by_option::*; 5 | pub use self::take_by_option::*; -------------------------------------------------------------------------------- /src/utils/skip_by_option.rs: -------------------------------------------------------------------------------- 1 | //! Creates an iterator that skips the first n elements, but only if n != None 2 | 3 | pub trait OptionalSkip where I: Iterator { 4 | fn skip_by_option(self, n: Option) -> SkipByOption; 5 | } 6 | 7 | impl OptionalSkip for I where I: Iterator { 8 | fn skip_by_option(self, n: Option) -> SkipByOption { 9 | SkipByOption::new(self, n) 10 | } 11 | } 12 | 13 | pub struct SkipByOption { 14 | iter: I, 15 | n: Option 16 | } 17 | 18 | impl SkipByOption { 19 | pub fn new(iter: I, n: Option) -> SkipByOption { 20 | SkipByOption { iter, n } 21 | } 22 | } 23 | 24 | impl Iterator for SkipByOption where I: Iterator { 25 | type Item = ::Item; 26 | 27 | fn next(&mut self) -> Option { 28 | match self.n { 29 | None | Some(0) => self.iter.next(), 30 | Some(n) => { 31 | self.n = None; 32 | self.iter.nth(n) 33 | } 34 | } 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::OptionalSkip; 41 | 42 | #[test] 43 | fn should_not_skips_elements_for_none() { 44 | assert_eq!( 45 | vec![1, 2, 3], 46 | vec![1, 2, 3].into_iter().skip_by_option(None).collect::>() 47 | ) 48 | } 49 | 50 | #[test] 51 | fn should_skips_elements_for_some() { 52 | assert_eq!( 53 | vec![3], 54 | vec![1, 2, 3].into_iter().skip_by_option(Some(2)).collect::>() 55 | ) 56 | } 57 | } -------------------------------------------------------------------------------- /src/utils/take_by_option.rs: -------------------------------------------------------------------------------- 1 | //! Creates an iterator that yields its first n elements, but only if n != None 2 | 3 | pub trait OptionalTake where I: Iterator { 4 | fn take_by_option(self, n: Option) -> TakeByOption; 5 | } 6 | 7 | impl OptionalTake for I where I: Iterator { 8 | fn take_by_option(self, n: Option) -> TakeByOption { 9 | TakeByOption::new(self, n) 10 | } 11 | } 12 | 13 | pub struct TakeByOption { 14 | iter: I, 15 | n: Option 16 | } 17 | 18 | impl TakeByOption { 19 | pub fn new(iter: I, n: Option) -> TakeByOption { 20 | TakeByOption { iter, n } 21 | } 22 | } 23 | 24 | impl Iterator for TakeByOption where I: Iterator { 25 | type Item = ::Item; 26 | 27 | fn next(&mut self) -> Option<::Item> { 28 | match self.n { 29 | None => self.iter.next(), 30 | Some(0) => None, 31 | Some(n) => { 32 | self.n = Some(n - 1); 33 | self.iter.next() 34 | } 35 | } 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::OptionalTake; 42 | 43 | #[test] 44 | fn should_not_affect_for_none() { 45 | assert_eq!( 46 | vec![1, 2, 3], 47 | vec![1, 2, 3].into_iter().take_by_option(None).collect::>() 48 | ) 49 | } 50 | 51 | #[test] 52 | fn should_takes_some_elements_for_some() { 53 | assert_eq!( 54 | vec![1, 2], 55 | vec![1, 2, 3].into_iter().take_by_option(Some(2)).collect::>() 56 | ) 57 | } 58 | } --------------------------------------------------------------------------------