├── .dockerignore ├── tests └── fixtures │ ├── projects │ ├── rendered │ │ ├── project-1 │ │ │ ├── Web.EMPTY.config │ │ │ ├── Web.TEST.yaml │ │ │ ├── Web.EMPTY.yaml │ │ │ ├── Web.ENVTYPE.yaml │ │ │ ├── Web.TEST2.yaml │ │ │ ├── Web.template.yaml │ │ │ ├── Web.TEST.config │ │ │ ├── Web.TEST2.config │ │ │ ├── Web.ENVTYPE.config │ │ │ └── Web.template.config │ │ └── project-2 │ │ │ ├── Web.EMPTY.config │ │ │ ├── Web.TEST.config │ │ │ ├── Web.TEST2.config │ │ │ ├── Web.ENVTYPE.config │ │ │ └── Web.template.config │ └── templates │ │ ├── .gitignore │ │ ├── project-3 │ │ └── .template.yaml.swp │ │ ├── project-4 │ │ ├── template.yaml │ │ ├── template.config │ │ └── template.properties │ │ ├── project-1 │ │ ├── Web.template.yaml │ │ └── Web.template.config │ │ └── project-2 │ │ └── Web.template.config │ └── configs │ ├── envTypes │ └── alpha.json │ ├── config.TEST.json │ ├── config.TEST2.json │ ├── config.EMPTY.json │ └── config.ENVTYPE.json ├── src ├── storage │ ├── mod.rs │ ├── cache.rs │ ├── lru.rs │ └── sqlite.rs ├── app │ ├── mod.rs │ ├── datadogstatsd.rs │ ├── fetch_actor.rs │ ├── config.rs │ ├── cli.rs │ ├── head_actor.rs │ └── server.rs ├── lib.rs ├── transform │ ├── helper_lowercase.rs │ ├── helper_url_rm_slash.rs │ ├── helper_yaml_string.rs │ ├── helper_url_add_slash.rs │ ├── helper_equal.rs │ ├── helper_or.rs │ ├── helper_url_rm_path.rs │ ├── helper_comma_delimited_list.rs │ └── mod.rs ├── error.rs ├── main.rs ├── template.rs ├── git.rs └── config.rs ├── .vscode └── settings.json ├── .cargo └── config ├── README.md.skt.md ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── README.md ├── CHANGELOG.md └── LICENSE /.dockerignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.EMPTY.config: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-2/Web.EMPTY.config: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod lru; 3 | pub mod sqlite; 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "WhiteSource Advise.Diff.BaseBranch": "master" 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/.gitignore: -------------------------------------------------------------------------------- 1 | *.* 2 | !*.template.* 3 | !.gitignore 4 | !template.* -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-3/.template.yaml.swp: -------------------------------------------------------------------------------- 1 | I'm just here to test that .swp files don't get picked up 2 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod config; 3 | pub mod datadogstatsd; 4 | mod fetch_actor; 5 | mod head_actor; 6 | pub mod server; 7 | -------------------------------------------------------------------------------- /tests/fixtures/configs/envTypes/alpha.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnvironmentType": "alpha", 3 | "ConfigData": { 4 | "EnvironmentType": "alpha" 5 | } 6 | } -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = ["-Ctarget-feature=+crt-static"] 3 | 4 | [build] 5 | rustflags = ["--cfg", "tokio_unstable"] -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-4/template.yaml: -------------------------------------------------------------------------------- 1 | #testing that templates starting with `template` and ending with `.yaml` get picked up by the regex -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-4/template.config: -------------------------------------------------------------------------------- 1 | #testing that templates starting with `template` and ending with `.config` get picked up by the regex -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-4/template.properties: -------------------------------------------------------------------------------- 1 | #testing that templates starting with `template` and ending with `.properties` get picked up by the regex -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.TEST.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: host-name\\TEST\" 3 | remove-slash: https://slash.com 4 | add-slash: https://nonslash.com/ 5 | no-slash: no-protocol.no-slash.com 6 | remove-path: https://path.com/path 7 | trailing-slash-remove-path: https://trailing-path.com/path 8 | lowercase: uppercase 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.EMPTY.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: host-name\\TEST\" 3 | remove-slash: https://slash.com 4 | add-slash: https://nonslash.com/ 5 | no-slash: no-protocol.no-slash.com 6 | remove-path: https://path.com/path 7 | trailing-slash-remove-path: https://trailing-path.com/path 8 | lowercase: uppercase 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.ENVTYPE.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: host-name\\TEST\" 3 | remove-slash: https://slash.com 4 | add-slash: https://nonslash.com/ 5 | no-slash: no-protocol.no-slash.com 6 | remove-path: https://path.com/path 7 | trailing-slash-remove-path: https://trailing-path.com/path 8 | lowercase: uppercase 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.TEST2.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: host-name\\TEST\" 3 | remove-slash: https://slash.com 4 | add-slash: https://nonslash.com/ 5 | no-slash: no-protocol.no-slash.com 6 | remove-path: https://path.com/path 7 | trailing-slash-remove-path: https://trailing-path.com/path 8 | lowercase: uppercase 9 | -------------------------------------------------------------------------------- /README.md.skt.md: -------------------------------------------------------------------------------- 1 | ```rust,skt-helpers 2 | extern crate hogan; 3 | #[macro_use] 4 | extern crate serde_json; 5 | 6 | fn main() {{ 7 | let handlebars = hogan::transform::handlebars(); 8 | 9 | {} 10 | 11 | let rendered = handlebars.render_template(template, &config); 12 | assert!(rendered.is_ok()); 13 | assert_eq!(&rendered.unwrap(), transformed); 14 | }} 15 | ``` -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.template.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: {{yaml-string DB.Endpoint}} 3 | remove-slash: {{url-rm-slash SlashService.endpoint}} 4 | add-slash: {{url-add-slash NonSlashService.endpoint}} 5 | no-slash: {{url-add-slash NonSlashService.notAnEndpoint}} 6 | remove-path: {{url-rm-path PathService.endpoint}} 7 | trailing-slash-remove-path: {{url-rm-path PathService.trailingSlash}} 8 | lowercase: {{lowercase UpperCaseString}} 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-1/Web.template.yaml: -------------------------------------------------------------------------------- 1 | Database: 2 | Endpoint: {{yaml-string DB.Endpoint}} 3 | remove-slash: {{url-rm-slash SlashService.endpoint}} 4 | add-slash: {{url-add-slash NonSlashService.endpoint}} 5 | no-slash: {{url-add-slash NonSlashService.notAnEndpoint}} 6 | remove-path: {{url-rm-path PathService.endpoint}} 7 | trailing-slash-remove-path: {{url-rm-path PathService.trailingSlash}} 8 | lowercase: {{lowercase UpperCaseString}} 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.TEST.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-2/Web.TEST.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.TEST2.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-2/Web.TEST2.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.ENVTYPE.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-2/Web.ENVTYPE.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-1/Web.template.config: -------------------------------------------------------------------------------- 1 | {{#if ConfigEnabled}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{/if}} -------------------------------------------------------------------------------- /tests/fixtures/projects/rendered/project-2/Web.template.config: -------------------------------------------------------------------------------- 1 | {{#if ConfigEnabled}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{/if}} -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-1/Web.template.config: -------------------------------------------------------------------------------- 1 | {{#if ConfigEnabled}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{/if}} -------------------------------------------------------------------------------- /tests/fixtures/projects/templates/project-2/Web.template.config: -------------------------------------------------------------------------------- 1 | {{#if ConfigEnabled}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{/if}} -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | 6 | pub mod config; 7 | pub mod error; 8 | pub mod git; 9 | pub mod template; 10 | pub mod transform; 11 | 12 | use regex::Regex; 13 | use std::path::{Path, PathBuf}; 14 | use walkdir::{DirEntry, WalkDir}; 15 | 16 | pub fn find_file_paths(path: &Path, filter: Regex) -> Box> { 17 | fn match_filter(entry: &DirEntry, filter: &Regex) -> bool { 18 | entry 19 | .file_name() 20 | .to_str() 21 | .map(|s| filter.is_match(s)) 22 | .unwrap_or(false) 23 | } 24 | 25 | println!("Finding Files: {:?}", path); 26 | println!("regex: /{}/", filter); 27 | 28 | Box::new( 29 | WalkDir::new(path) 30 | .into_iter() 31 | .filter_map(|e| e.ok()) 32 | .filter(|e| e.file_type().is_file()) 33 | .filter(move |e| match_filter(e, &filter)) 34 | .map(|e| e.path().to_path_buf()), 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/configs/config.TEST.json: -------------------------------------------------------------------------------- 1 | { 2 | "Environment": "TEST", 3 | "ConfigData": { 4 | "ConfigEnabled": true, 5 | "Region": { 6 | "Key": "TEST" 7 | }, 8 | "DB": { 9 | "Endpoint": "host-name\\TEST\"" 10 | }, 11 | "Memcache": { 12 | "Servers": [ 13 | { 14 | "Endpoint": "192.168.1.100", 15 | "Port": "1122" 16 | }, 17 | { 18 | "Endpoint": "192.168.1.101", 19 | "Port": "1122" 20 | }, 21 | { 22 | "Endpoint": "192.168.1.102", 23 | "Port": "1122" 24 | } 25 | ] 26 | }, 27 | "NonSlashService": { 28 | "endpoint": "https://nonslash.com", 29 | "notAnEndpoint": "no-protocol.no-slash.com" 30 | }, 31 | "SlashService": { 32 | "endpoint": "https://slash.com/" 33 | }, 34 | "PathService": { 35 | "endpoint": "https://path.com/path/remove-this", 36 | "trailingSlash": "https://trailing-path.com/path/should-still-remove/" 37 | }, 38 | "UpperCaseString": "UPPERCASE" 39 | } 40 | } -------------------------------------------------------------------------------- /tests/fixtures/configs/config.TEST2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Environment": "TEST2", 3 | "ConfigData": { 4 | "ConfigEnabled": true, 5 | "Region": { 6 | "Key": "TEST2" 7 | }, 8 | "DB": { 9 | "Endpoint": "host-name\\TEST\"" 10 | }, 11 | "Memcache": { 12 | "Servers": [ 13 | { 14 | "Endpoint": "192.168.1.100", 15 | "Port": "1122" 16 | }, 17 | { 18 | "Endpoint": "192.168.1.101", 19 | "Port": "1122" 20 | }, 21 | { 22 | "Endpoint": "192.168.1.102", 23 | "Port": "1122" 24 | } 25 | ] 26 | }, 27 | "NonSlashService": { 28 | "endpoint": "https://nonslash.com", 29 | "notAnEndpoint": "no-protocol.no-slash.com" 30 | }, 31 | "SlashService": { 32 | "endpoint": "https://slash.com/" 33 | }, 34 | "PathService": { 35 | "endpoint": "https://path.com/path/remove-this", 36 | "trailingSlash": "https://trailing-path.com/path/should-still-remove/" 37 | }, 38 | "UpperCaseString": "UPPERCASE" 39 | } 40 | } -------------------------------------------------------------------------------- /tests/fixtures/configs/config.EMPTY.json: -------------------------------------------------------------------------------- 1 | { 2 | "Environment": "EMPTY", 3 | "ConfigData": { 4 | "ConfigEnabled": false, 5 | "Region": { 6 | "Key": "EMPTY" 7 | }, 8 | "DB": { 9 | "Endpoint": "host-name\\TEST\"" 10 | }, 11 | "Memcache": { 12 | "Servers": [ 13 | { 14 | "Endpoint": "192.168.1.100", 15 | "Port": "1122" 16 | }, 17 | { 18 | "Endpoint": "192.168.1.101", 19 | "Port": "1122" 20 | }, 21 | { 22 | "Endpoint": "192.168.1.102", 23 | "Port": "1122" 24 | } 25 | ] 26 | }, 27 | "NonSlashService": { 28 | "endpoint": "https://nonslash.com", 29 | "notAnEndpoint": "no-protocol.no-slash.com" 30 | }, 31 | "SlashService": { 32 | "endpoint": "https://slash.com/" 33 | }, 34 | "PathService": { 35 | "endpoint": "https://path.com/path/remove-this", 36 | "trailingSlash": "https://trailing-path.com/path/should-still-remove/" 37 | }, 38 | "UpperCaseString": "UPPERCASE" 39 | } 40 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Avoid checking in db files 13 | *.db 14 | *.json 15 | 16 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 17 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 18 | 19 | ### VisualStudioCode ### 20 | .vscode/* 21 | !.vscode/settings.json 22 | !.vscode/tasks.json 23 | !.vscode/launch.json 24 | !.vscode/extensions.json 25 | !.vscode/*.code-snippets 26 | 27 | # Local History for Visual Studio Code 28 | .history/ 29 | 30 | # Built Visual Studio Code Extensions 31 | *.vsix 32 | 33 | ### VisualStudioCode Patch ### 34 | # Ignore all local history of files 35 | .history 36 | .ionide 37 | 38 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode 39 | -------------------------------------------------------------------------------- /tests/fixtures/configs/config.ENVTYPE.json: -------------------------------------------------------------------------------- 1 | { 2 | "Environment": "ENVTYPE", 3 | "EnvironmentType": "alpha", 4 | "ConfigData": { 5 | "ConfigEnabled": true, 6 | "Region": { 7 | "Key": "ENVTYPE" 8 | }, 9 | "DB": { 10 | "Endpoint": "host-name\\TEST\"" 11 | }, 12 | "Memcache": { 13 | "Servers": [ 14 | { 15 | "Endpoint": "192.168.1.100", 16 | "Port": "1122" 17 | }, 18 | { 19 | "Endpoint": "192.168.1.101", 20 | "Port": "1122" 21 | }, 22 | { 23 | "Endpoint": "192.168.1.102", 24 | "Port": "1122" 25 | } 26 | ] 27 | }, 28 | "NonSlashService": { 29 | "endpoint": "https://nonslash.com", 30 | "notAnEndpoint": "no-protocol.no-slash.com" 31 | }, 32 | "SlashService": { 33 | "endpoint": "https://slash.com/" 34 | }, 35 | "PathService": { 36 | "endpoint": "https://path.com/path/remove-this", 37 | "trailingSlash": "https://trailing-path.com/path/should-still-remove/" 38 | }, 39 | "UpperCaseString": "UPPERCASE" 40 | } 41 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [[bin]] 2 | name = 'hogan' 3 | path = 'src/main.rs' 4 | doc = false 5 | 6 | [package] 7 | name = 'hogan' 8 | version = '0.15.0' 9 | authors = [ 10 | 'Jonathan Morley ', 11 | 'Josh Comer ', 12 | ] 13 | edition = '2021' 14 | 15 | [dependencies] 16 | actix-web = '4.3' 17 | anyhow = '1.0' 18 | bincode = '1.3' 19 | compression = '0.1' 20 | dogstatsd = '0.7' 21 | futures = '0.3' 22 | handlebars = '4.3' 23 | itertools = '0.10' 24 | json-patch = '0.3' 25 | lazy_static = '1' 26 | log = '0.4' 27 | lru = '0.9' 28 | parking_lot = '0.12' 29 | riker = '0.4' 30 | riker-patterns = '0.4' 31 | serde_derive = '1.0' 32 | serde_json = '1.0' 33 | shellexpand = '3.0' 34 | stderrlog = '0.5' 35 | structopt = '0.3' 36 | tempfile = '3' 37 | thiserror = '1.0' 38 | url = '2' 39 | walkdir = '2' 40 | zip = '0.6' 41 | 42 | [dependencies.rusqlite] 43 | version = '0.28' 44 | features = ['bundled'] 45 | 46 | [dependencies.git2] 47 | version = '0.16' 48 | features = ['vendored-openssl'] 49 | 50 | [dependencies.regex] 51 | version = '1.7' 52 | default-features = false 53 | 54 | [dependencies.serde] 55 | version = '1.0' 56 | features = ['rc'] 57 | 58 | [dependencies.uuid] 59 | version = '1.2' 60 | features = ['v4'] 61 | 62 | [dev-dependencies] 63 | assert_cmd = '2.0' 64 | dir-diff = '0.3' 65 | fs_extra = '1' 66 | predicates = '2.1' 67 | -------------------------------------------------------------------------------- /src/transform/helper_lowercase.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use serde_json::value::Value as Json; 3 | 4 | #[derive(Clone, Copy)] 5 | pub struct LowercaseHelper; 6 | 7 | impl HelperDef for LowercaseHelper { 8 | fn call<'reg: 'rc, 'rc, 'ctx>( 9 | &self, 10 | h: &Helper<'reg, 'rc>, 11 | _: &'reg Handlebars, 12 | _: &'ctx Context, 13 | _: &mut RenderContext<'reg, 'ctx>, 14 | out: &mut dyn Output, 15 | ) -> HelperResult { 16 | let value = h 17 | .param(0) 18 | .ok_or_else(|| RenderError::new("Param not found for helper \"lowercase\""))?; 19 | 20 | match *value.value() { 21 | Json::String(ref s) => { 22 | out.write(&s.to_lowercase())?; 23 | Ok(()) 24 | } 25 | Json::Null => Ok(()), 26 | _ => Err(RenderError::new(format!( 27 | "Param type is not string for helper \"lowercase\": {:?}", 28 | value 29 | ))), 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod test { 36 | use super::*; 37 | use crate::transform::test::test_against_configs; 38 | 39 | #[test] 40 | fn test_lowercase() { 41 | let mut handlebars = Handlebars::new(); 42 | handlebars.register_helper("lowercase", Box::new(LowercaseHelper)); 43 | 44 | test_against_configs(&handlebars, "{{lowercase UpperCaseString}}", "uppercase"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # We need to use the Rust build image, because 2 | # we need the Rust compile and Cargo tooling 3 | FROM clux/muslrust:stable as build 4 | 5 | # Install cmake as it is not included in muslrust, but is needed by libssh2-sys 6 | RUN apt-get update && apt-get install -y \ 7 | cmake \ 8 | --no-install-recommends && \ 9 | rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /app 12 | # Creates a dummy project used to grab dependencies 13 | RUN USER=root cargo init --bin 14 | 15 | # Copies over *only* your manifests 16 | COPY ./Cargo.* ./ 17 | 18 | # Builds your dependencies and removes the 19 | # fake source code from the dummy project 20 | RUN cargo build --release 21 | RUN rm src/*.rs 22 | RUN rm target/x86_64-unknown-linux-musl/release/hogan 23 | 24 | # Copies only your actual source code to 25 | # avoid invalidating the cache at all 26 | COPY ./src ./src 27 | 28 | # Builds again, this time it'll just be 29 | # your actual source files being built 30 | RUN cargo build --release 31 | 32 | FROM alpine:latest as certs 33 | RUN apk --update add ca-certificates 34 | 35 | # Create a new stage with a minimal image 36 | # because we already have a binary built 37 | FROM alpine:latest 38 | 39 | RUN apk --no-cache add git openssh 40 | 41 | # Copies standard SSL certs from the "build" stage 42 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 43 | 44 | # Copies the binary from the "build" stage 45 | COPY --from=build /app/target/x86_64-unknown-linux-musl/release/hogan /bin/ 46 | 47 | # Configures the startup! 48 | ENTRYPOINT ["/bin/hogan"] 49 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error, Clone)] 4 | pub enum HoganError { 5 | #[error("There was an error with the underlying git repository. {msg}")] 6 | GitError { msg: String }, 7 | #[error("The requested SHA {sha} was not found in the git repo")] 8 | UnknownSHA { sha: String }, 9 | #[error("The requested branch {branch} was not found in the git repo")] 10 | UnknownBranch { branch: String }, 11 | #[error("The requested environment {env} was not found in {sha}")] 12 | UnknownEnvironment { sha: String, env: String }, 13 | #[error("There was a problem with the provided template")] 14 | InvalidTemplate { msg: String, env: String }, 15 | #[error("The request was malformed")] 16 | BadRequest, 17 | #[error("Request timed out due to internal congestion")] 18 | InternalTimeout, 19 | #[error("An error occurred parsing configuration {param}: {msg}")] 20 | InvalidConfiguration { param: String, msg: String }, 21 | #[error("An unknown error occurred. {msg}")] 22 | UnknownError { msg: String }, 23 | } 24 | 25 | impl From for HoganError { 26 | fn from(e: git2::Error) -> Self { 27 | HoganError::GitError { 28 | msg: e.message().to_owned(), 29 | } 30 | } 31 | } 32 | 33 | impl From for HoganError { 34 | fn from(e: anyhow::Error) -> Self { 35 | match e.downcast() { 36 | Ok(e) => e, 37 | Err(e) => { 38 | warn!("Bad cast to a HoganError {:?}", e); 39 | HoganError::UnknownError { 40 | msg: format!("Error {:?}", e), 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/transform/helper_url_rm_slash.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use serde_json::value::Value as Json; 3 | 4 | #[derive(Clone, Copy)] 5 | pub struct UrlRmSlashHelper; 6 | 7 | impl HelperDef for UrlRmSlashHelper { 8 | // Removes the trailing slash on an endpoint 9 | fn call<'reg: 'rc, 'rc, 'ctx>( 10 | &self, 11 | h: &Helper<'reg, 'rc>, 12 | _: &'reg Handlebars, 13 | _: &'ctx Context, 14 | _: &mut RenderContext<'reg, 'ctx>, 15 | out: &mut dyn Output, 16 | ) -> HelperResult { 17 | let value = h 18 | .param(0) 19 | .ok_or_else(|| RenderError::new("Param not found for helper \"url-rm-slash\""))?; 20 | 21 | match *value.value() { 22 | Json::String(ref s) => { 23 | if s.ends_with('/') { 24 | out.write(&s[..s.len() - 1])?; 25 | } else { 26 | out.write(s)?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | Json::Null => Ok(()), 32 | _ => Err(RenderError::new(format!( 33 | "Param type is not string for helper \"url-rm-slash\": {:?}", 34 | value, 35 | ))), 36 | } 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod test { 42 | use super::*; 43 | use crate::transform::test::test_against_configs; 44 | 45 | #[test] 46 | fn test_url_rm_slash() { 47 | let mut handlebars = Handlebars::new(); 48 | handlebars.register_helper("url-rm-slash", Box::new(UrlRmSlashHelper)); 49 | 50 | test_against_configs( 51 | &handlebars, 52 | "{{url-rm-slash SlashService.endpoint}}", 53 | "https://slash.com", 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[macro_use] 4 | extern crate lazy_static; 5 | 6 | use crate::app::cli; 7 | use crate::app::config::{App, AppCommand}; 8 | use crate::app::server; 9 | use anyhow::{Context, Result}; 10 | 11 | use structopt::StructOpt; 12 | 13 | mod app; 14 | mod storage; 15 | 16 | fn main() -> Result<()> { 17 | let opt = App::from_args(); 18 | 19 | stderrlog::new() 20 | .module(module_path!()) 21 | .verbosity(opt.verbosity + 2) 22 | .timestamp(stderrlog::Timestamp::Millisecond) 23 | .init() 24 | .with_context(|| "Error initializing logging")?; 25 | 26 | match opt.cmd { 27 | AppCommand::Transform { 28 | templates_path, 29 | environments_regex, 30 | templates_regex, 31 | common, 32 | ignore_existing, 33 | } => { 34 | cli::cli( 35 | templates_path, 36 | environments_regex, 37 | templates_regex, 38 | common, 39 | ignore_existing, 40 | )?; 41 | } 42 | AppCommand::Server { 43 | common, 44 | port, 45 | address, 46 | environments_regex, 47 | datadog, 48 | environment_pattern, 49 | db_path, 50 | fetch_poller, 51 | allow_fetch, 52 | db_max_age, 53 | cache_size, 54 | } => { 55 | server::start_up_server( 56 | common, 57 | port, 58 | address, 59 | environments_regex, 60 | datadog, 61 | environment_pattern, 62 | db_path, 63 | cache_size, 64 | fetch_poller, 65 | allow_fetch, 66 | db_max_age, 67 | )?; 68 | } 69 | } 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/transform/helper_yaml_string.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use serde_json::value::Value as Json; 3 | 4 | #[derive(Clone, Copy)] 5 | pub struct YamlStringHelper; 6 | 7 | impl HelperDef for YamlStringHelper { 8 | // Escapes strings to that they can be safely used inside yaml (And JSON for that matter). 9 | fn call<'reg: 'rc, 'rc, 'ctx>( 10 | &self, 11 | h: &Helper<'reg, 'rc>, 12 | _: &'reg Handlebars, 13 | _: &'ctx Context, 14 | _: &mut RenderContext<'reg, 'ctx>, 15 | out: &mut dyn Output, 16 | ) -> HelperResult { 17 | let value = h 18 | .param(0) 19 | .ok_or_else(|| RenderError::new("Param not found for helper \"yaml-string\""))?; 20 | 21 | match *value.value() { 22 | ref s @ Json::String(_) => { 23 | let mut stringified = serde_json::to_string(&s).unwrap(); 24 | if stringified.starts_with('"') { 25 | stringified.remove(0); 26 | } 27 | 28 | if stringified.ends_with('"') { 29 | stringified.pop(); 30 | } 31 | 32 | out.write(&stringified)?; 33 | 34 | Ok(()) 35 | } 36 | Json::Null => Ok(()), 37 | _ => Err(RenderError::new(format!( 38 | "Param type is not string for helper \"yaml-string\": {:?}", 39 | value, 40 | ))), 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod test { 47 | use super::*; 48 | use crate::transform::test::test_against_configs; 49 | 50 | #[test] 51 | fn test_yaml_string() { 52 | let mut handlebars = Handlebars::new(); 53 | handlebars.register_helper("yaml-string", Box::new(YamlStringHelper)); 54 | 55 | test_against_configs( 56 | &handlebars, 57 | "{{yaml-string DB.Endpoint}}", 58 | r#"host-name\\TEST\""#, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/transform/helper_url_add_slash.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use serde_json::value::Value as Json; 3 | use url::Url; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct UrlAddSlashHelper; 7 | 8 | impl HelperDef for UrlAddSlashHelper { 9 | // Adds the trailing slashes on an endpoint 10 | fn call<'reg: 'rc, 'rc, 'ctx>( 11 | &self, 12 | h: &Helper<'reg, 'rc>, 13 | _: &'reg Handlebars, 14 | _: &'ctx Context, 15 | _: &mut RenderContext<'reg, 'ctx>, 16 | out: &mut dyn Output, 17 | ) -> HelperResult { 18 | let value = h 19 | .param(0) 20 | .ok_or_else(|| RenderError::new("Param not found for helper \"url-add-slash\""))?; 21 | 22 | match *value.value() { 23 | Json::String(ref s) => { 24 | let output = if Url::parse(s).is_ok() && !s.ends_with('/') { 25 | format!("{}/", s) 26 | } else { 27 | s.clone() 28 | }; 29 | 30 | out.write(&output)?; 31 | 32 | Ok(()) 33 | } 34 | Json::Null => Ok(()), 35 | _ => Err(RenderError::new(format!( 36 | "Param type is not string for helper \"url-add-slash\": {:?}", 37 | value, 38 | ))), 39 | } 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod test { 45 | use super::*; 46 | use crate::transform::test::test_against_configs; 47 | 48 | #[test] 49 | fn test_url_add_slash() { 50 | let mut handlebars = Handlebars::new(); 51 | handlebars.register_helper("url-add-slash", Box::new(UrlAddSlashHelper)); 52 | 53 | let templates = vec![ 54 | ( 55 | "{{url-add-slash NonSlashService.endpoint}}", 56 | "https://nonslash.com/", 57 | ), 58 | ( 59 | "{{url-add-slash NonSlashService.notAnEndpoint}}", 60 | "no-protocol.no-slash.com", 61 | ), 62 | ]; 63 | 64 | for (template, expected) in templates { 65 | test_against_configs(&handlebars, template, expected) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/transform/helper_equal.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct EqualHelper; 5 | 6 | impl HelperDef for EqualHelper { 7 | fn call<'reg: 'rc, 'rc, 'ctx>( 8 | &self, 9 | h: &Helper<'reg, 'rc>, 10 | r: &'reg Handlebars, 11 | ctx: &'ctx Context, 12 | rc: &mut RenderContext<'reg, 'ctx>, 13 | out: &mut dyn Output, 14 | ) -> HelperResult { 15 | let lvalue = h 16 | .param(0) 17 | .ok_or_else(|| RenderError::new("Left param not found for helper \"equal\""))? 18 | .value(); 19 | let rvalue = h 20 | .param(1) 21 | .ok_or_else(|| RenderError::new("Right param not found for helper \"equal\""))? 22 | .value(); 23 | 24 | let comparison = lvalue == rvalue; 25 | 26 | if h.is_block() { 27 | let template = if comparison { 28 | h.template() 29 | } else { 30 | h.inverse() 31 | }; 32 | 33 | match template { 34 | Some(t) => t.render(r, ctx, rc, out), 35 | None => Ok(()), 36 | } 37 | } else { 38 | if comparison { 39 | out.write(&comparison.to_string())?; 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod test { 49 | use super::*; 50 | use crate::transform::test::test_against_configs; 51 | 52 | #[test] 53 | fn test_equal() { 54 | let mut handlebars = Handlebars::new(); 55 | handlebars.register_helper("equal", Box::new(EqualHelper)); 56 | handlebars.register_helper("eq", Box::new(EqualHelper)); 57 | 58 | let templates = vec![ 59 | (r#"{{#equal Region.Key "TEST"}}Foo{{/equal}}"#, "Foo"), 60 | (r#"{{#equal Region.Key null}}{{else}}Bar{{/equal}}"#, "Bar"), 61 | (r#"{{#eq Region.Key "TEST"}}Foo{{/eq}}"#, "Foo"), 62 | (r#"{{#eq Region.Key null}}{{else}}Bar{{/eq}}"#, "Bar"), 63 | (r#"{{#if (equal Region.Key "TEST")}}Foo{{/if}}"#, "Foo"), 64 | ( 65 | r#"{{#if (equal Region.Key null)}}{{else}}Bar{{/if}}"#, 66 | "Bar", 67 | ), 68 | (r#"{{#if (eq Region.Key "TEST")}}Foo{{/if}}"#, "Foo"), 69 | (r#"{{#if (eq Region.Key null)}}{{else}}Bar{{/if}}"#, "Bar"), 70 | ]; 71 | 72 | for (template, expected) in templates { 73 | test_against_configs(&handlebars, template, expected) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | RUST_LOG: info 9 | RUST_BACKTRACE: 1 10 | 11 | jobs: 12 | publish_github: 13 | name: Publish to GitHub Releases 14 | permissions: 15 | contents: write 16 | packages: write 17 | repository-projects: read 18 | deployments: write 19 | actions: read 20 | issues: read 21 | statuses: write 22 | strategy: 23 | matrix: 24 | target: 25 | # Rustc's Tier 1 platforms 26 | # https://doc.rust-lang.org/nightly/rustc/platform-support.html#tier-1-with-host-tools 27 | # Windows gnu is not currently working 28 | # - i686-pc-windows-gnu 29 | - i686-pc-windows-msvc 30 | - i686-unknown-linux-gnu 31 | - x86_64-apple-darwin 32 | # Windows gnu is not currently working 33 | # - x86_64-pc-windows-gnu 34 | - x86_64-pc-windows-msvc 35 | - x86_64-unknown-linux-gnu 36 | # Select tier 2 platforms 37 | # https://doc.rust-lang.org/nightly/rustc/platform-support.html#tier-2-with-host-tools 38 | - aarch64-apple-darwin 39 | # Windows ARM is not currently working 40 | # - aarch64-pc-windows-msvc 41 | - x86_64-unknown-linux-musl 42 | # Testability according to cross 43 | # https://github.com/rust-embedded/cross#supported-targets 44 | include: 45 | # - target: i686-pc-windows-gnu 46 | # os: windows-latest 47 | - target: i686-pc-windows-msvc 48 | os: windows-latest 49 | - target: i686-unknown-linux-gnu 50 | os: ubuntu-latest 51 | - target: x86_64-apple-darwin 52 | os: macos-latest 53 | # - target: x86_64-pc-windows-gnu 54 | # os: windows-latest 55 | - target: x86_64-pc-windows-msvc 56 | os: windows-latest 57 | - target: x86_64-unknown-linux-gnu 58 | os: ubuntu-latest 59 | - target: aarch64-apple-darwin 60 | os: macos-latest 61 | # - target: aarch64-pc-windows-msvc 62 | # os: windows-latest 63 | - target: x86_64-unknown-linux-musl 64 | os: ubuntu-latest 65 | runs-on: ${{ matrix.os }} 66 | steps: 67 | - name: Checkout 68 | uses: actions/checkout@v3 69 | - name: Install Rust 70 | uses: actions-rs/toolchain@v1 71 | with: 72 | toolchain: stable 73 | target: ${{ matrix.target }} 74 | profile: minimal 75 | - name: Rust Cache 76 | uses: Swatinem/rust-cache@v2.2.1 77 | with: 78 | key: ${{ matrix.target }} 79 | - name: Archive Release 80 | uses: taiki-e/upload-rust-binary-action@v1 81 | with: 82 | bin: hogan 83 | target: ${{ matrix.target }} 84 | env: 85 | # (required) GitHub token for creating GitHub Releases. 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | -------------------------------------------------------------------------------- /src/transform/helper_or.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct OrHelper; 5 | 6 | impl HelperDef for OrHelper { 7 | fn call<'reg: 'rc, 'rc, 'ctx>( 8 | &self, 9 | h: &Helper<'reg, 'rc>, 10 | r: &'reg Handlebars, 11 | ctx: &'ctx Context, 12 | rc: &mut RenderContext<'reg, 'ctx>, 13 | out: &mut dyn Output, 14 | ) -> HelperResult { 15 | if h.params().len() < 2 { 16 | return Err(RenderError::new("'or' requires at least 2 parameters")); 17 | } 18 | 19 | let comparison = h 20 | .params() 21 | .iter() 22 | .any(|p| p.value().as_str().map_or(false, |v| !v.is_empty())); 23 | 24 | if h.is_block() { 25 | let template = if comparison { 26 | h.template() 27 | } else { 28 | h.inverse() 29 | }; 30 | 31 | match template { 32 | Some(t) => t.render(r, ctx, rc, out), 33 | None => Ok(()), 34 | } 35 | } else { 36 | if comparison { 37 | out.write(&comparison.to_string())?; 38 | } 39 | 40 | Ok(()) 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod test { 47 | use super::*; 48 | use crate::transform::helper_equal::EqualHelper; 49 | use crate::transform::test::test_against_configs; 50 | use crate::transform::test::test_error_against_configs; 51 | 52 | #[test] 53 | fn test_or() { 54 | let mut handlebars = Handlebars::new(); 55 | handlebars.register_helper("eq", Box::new(EqualHelper)); 56 | handlebars.register_helper("or", Box::new(OrHelper)); 57 | 58 | let templates = vec![ 59 | ( 60 | r#"{{#or (eq Region.Key "TEST") (eq Region.Key "TEST2")}}Foo{{/or}}"#, 61 | "Foo", 62 | ), 63 | ( 64 | r#"{{#or (eq Region.Key null) (eq Region.Key "NO")}}{{else}}Bar{{/or}}"#, 65 | "Bar", 66 | ), 67 | ( 68 | r#"{{#if (or (eq Region.Key "TEST") (eq Region.Key "TEST2"))}}Foo{{/if}}"#, 69 | "Foo", 70 | ), 71 | ( 72 | r#"{{#if (or (eq Region.Key null) (eq Region.Key "NO"))}}{{else}}Bar{{/if}}"#, 73 | "Bar", 74 | ), 75 | ( 76 | r#"{{#or (eq Region.Key "NO") (eq Region.Key "TEST2") (eq Region.Key "TEST")}}Foo{{/or}}"#, 77 | "Foo", 78 | ), 79 | ]; 80 | 81 | let error_templates = vec![( 82 | r#"{{#or (eq Region.Key "NO") }}Foo{{/or}}"#, 83 | "'or' requires at least 2 parameters", 84 | )]; 85 | 86 | for (template, expected) in templates { 87 | test_against_configs(&handlebars, template, expected) 88 | } 89 | 90 | for (template, expected) in error_templates { 91 | test_error_against_configs(&handlebars, template, expected) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/transform/helper_url_rm_path.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use serde_json::value::Value as Json; 3 | use url::Url; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct UrlRmPathHelper; 7 | 8 | impl HelperDef for UrlRmPathHelper { 9 | // Removes the last slash plus content to the end of the string 10 | fn call<'reg: 'rc, 'rc, 'ctx>( 11 | &self, 12 | h: &Helper<'reg, 'rc>, 13 | _: &'reg Handlebars, 14 | _: &'ctx Context, 15 | _: &mut RenderContext<'reg, 'ctx>, 16 | out: &mut dyn Output, 17 | ) -> HelperResult { 18 | let value = h 19 | .param(0) 20 | .ok_or_else(|| RenderError::new("Param not found for helper \"url-rm-path\""))?; 21 | 22 | match *value.value() { 23 | Json::String(ref s) => { 24 | let url = if s.ends_with('/') { 25 | &s[..s.len() - 1] 26 | } else { 27 | s 28 | }; 29 | 30 | match Url::parse(url) { 31 | Ok(ref mut url) => { 32 | if let Ok(ref mut paths) = url.path_segments_mut() { 33 | paths.pop(); 34 | } 35 | 36 | let mut url_str = url.as_str(); 37 | if url_str.ends_with('/') { 38 | url_str = &url_str[..url_str.len() - 1]; 39 | } 40 | 41 | out.write(url_str)?; 42 | 43 | Ok(()) 44 | } 45 | _ => { 46 | out.write(s)?; 47 | Ok(()) 48 | } 49 | } 50 | } 51 | Json::Null => Ok(()), 52 | _ => Err(RenderError::new(format!( 53 | "Param type is not string for helper \"url-rm-path\": {:?}", 54 | value 55 | ))), 56 | } 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod test { 62 | use super::*; 63 | use crate::transform::test::test_against_configs; 64 | 65 | #[test] 66 | fn test_url_rm_path() { 67 | let mut handlebars = Handlebars::new(); 68 | handlebars.register_helper("url-rm-path", Box::new(UrlRmPathHelper)); 69 | 70 | let templates = vec![ 71 | ( 72 | "{{url-rm-path PathService.endpoint}}", 73 | "https://path.com/path", 74 | ), 75 | ( 76 | "{{url-rm-path PathService.trailingSlash}}", 77 | "https://trailing-path.com/path", 78 | ), 79 | ]; 80 | 81 | for (template, expected) in templates { 82 | test_against_configs(&handlebars, template, expected) 83 | } 84 | } 85 | 86 | #[test] 87 | fn test_double_url_rm_path() { 88 | let mut handlebars = Handlebars::new(); 89 | handlebars.register_helper("url-rm-path", Box::new(UrlRmPathHelper)); 90 | 91 | test_against_configs( 92 | &handlebars, 93 | "{{url-rm-path (url-rm-path PathService.trailingSlash)}}", 94 | "https://trailing-path.com", 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/transform/helper_comma_delimited_list.rs: -------------------------------------------------------------------------------- 1 | use handlebars::*; 2 | use itertools::join; 3 | use serde_json::value::Value as Json; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct CommaDelimitedListHelper; 7 | 8 | impl HelperDef for CommaDelimitedListHelper { 9 | // Change an array of items into a comma seperated list with formatting 10 | // Usage: {{#comma-list array}}{{elementAttribute}}:{{attribute2}}{{/comma-list}} 11 | fn call<'reg: 'rc, 'rc, 'ctx>( 12 | &self, 13 | h: &Helper<'reg, 'rc>, 14 | r: &'reg Handlebars, 15 | ctx: &'ctx Context, 16 | rc: &mut RenderContext<'reg, 'ctx>, 17 | out: &mut dyn Output, 18 | ) -> HelperResult { 19 | let value = h 20 | .param(0) 21 | .ok_or_else(|| RenderError::new("Param not found for helper \"comma-list\""))?; 22 | 23 | match h.template() { 24 | Some(template) => match *value.value() { 25 | Json::Array(ref list) => { 26 | let mut render_list = Vec::new(); 27 | 28 | for (i, item) in list.iter().enumerate() { 29 | let mut local_rc = rc.clone(); 30 | let block_rc = local_rc.block_mut().unwrap(); 31 | if let Some(inner_path) = value.context_path() { 32 | let block_path = block_rc.base_path_mut(); 33 | block_path.append(&mut inner_path.to_owned()); 34 | block_path.push(i.to_string()); 35 | } 36 | 37 | if let Some(block_param) = h.block_param() { 38 | let mut new_block = BlockContext::new(); 39 | let mut block_params = BlockParams::new(); 40 | block_params.add_value(block_param, to_json(item))?; 41 | new_block.set_block_params(block_params); 42 | local_rc.push_block(new_block); 43 | 44 | render_list.push(template.renders(r, ctx, &mut local_rc)?); 45 | } else { 46 | render_list.push(template.renders(r, ctx, &mut local_rc)?); 47 | } 48 | } 49 | out.write(&join(&render_list, ","))?; 50 | 51 | Ok(()) 52 | } 53 | Json::Null => Ok(()), 54 | _ => Err(RenderError::new(format!( 55 | "Param type is not array for helper \"comma-list\": {:?}", 56 | value 57 | ))), 58 | }, 59 | None => Ok(()), 60 | } 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod test { 66 | use super::*; 67 | use crate::transform::test::test_against_configs; 68 | 69 | #[test] 70 | fn test_comma_list() { 71 | let mut handlebars = Handlebars::new(); 72 | handlebars.register_helper("comma-list", Box::new(CommaDelimitedListHelper)); 73 | 74 | test_against_configs( 75 | &handlebars, 76 | "{{#comma-list Memcache.Servers}}{{Endpoint}}:{{Port}}{{/comma-list}}", 77 | "192.168.1.100:1122,192.168.1.101:1122,192.168.1.102:1122", 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/transform/mod.rs: -------------------------------------------------------------------------------- 1 | use handlebars::Handlebars; 2 | 3 | mod helper_comma_delimited_list; 4 | mod helper_equal; 5 | mod helper_lowercase; 6 | mod helper_or; 7 | mod helper_url_add_slash; 8 | mod helper_url_rm_path; 9 | mod helper_url_rm_slash; 10 | mod helper_yaml_string; 11 | 12 | use self::helper_comma_delimited_list::CommaDelimitedListHelper; 13 | use self::helper_equal::EqualHelper; 14 | use self::helper_lowercase::LowercaseHelper; 15 | use self::helper_or::OrHelper; 16 | use self::helper_url_add_slash::UrlAddSlashHelper; 17 | use self::helper_url_rm_path::UrlRmPathHelper; 18 | use self::helper_url_rm_slash::UrlRmSlashHelper; 19 | use self::helper_yaml_string::YamlStringHelper; 20 | 21 | //This fn was changed here https://github.com/sunng87/handlebars-rust/pull/366 which added additional characters to the list 22 | //To maintain backwards compatibility we are reverting to the original default escape fn 23 | pub fn old_escape_html(s: &str) -> String { 24 | let mut output = String::new(); 25 | 26 | for c in s.chars() { 27 | match c { 28 | '<' => output.push_str("<"), 29 | '>' => output.push_str(">"), 30 | '"' => output.push_str("""), 31 | '&' => output.push_str("&"), 32 | _ => output.push(c), 33 | } 34 | } 35 | output 36 | } 37 | 38 | pub fn handlebars<'a>(strict: bool) -> Handlebars<'a> { 39 | let mut handlebars = Handlebars::new(); 40 | handlebars.set_strict_mode(strict); 41 | handlebars.register_helper("comma-list", Box::new(CommaDelimitedListHelper)); 42 | handlebars.register_helper("equal", Box::new(EqualHelper)); 43 | handlebars.register_helper("eq", Box::new(EqualHelper)); 44 | handlebars.register_helper("lowercase", Box::new(LowercaseHelper)); 45 | handlebars.register_helper("or", Box::new(OrHelper)); 46 | handlebars.register_helper("url-add-slash", Box::new(UrlAddSlashHelper)); 47 | handlebars.register_helper("url-rm-path", Box::new(UrlRmPathHelper)); 48 | handlebars.register_helper("url-rm-slash", Box::new(UrlRmSlashHelper)); 49 | handlebars.register_helper("yaml-string", Box::new(YamlStringHelper)); 50 | handlebars.register_escape_fn(old_escape_html); 51 | handlebars 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | use super::*; 57 | use serde_json::{self, Value}; 58 | 59 | fn config_fixture() -> Value { 60 | let mut config: Value = serde_json::from_str(&include_str!( 61 | "../../tests/fixtures/configs/config.TEST.json" 62 | )) 63 | .unwrap(); 64 | config["ConfigData"].take() 65 | } 66 | 67 | pub(crate) fn test_against_configs(handlebars: &Handlebars, template: &str, expected: &str) { 68 | let config_rendered = handlebars.render_template(template, &config_fixture()); 69 | assert!(config_rendered.is_ok()); 70 | assert_eq!(&config_rendered.unwrap(), expected); 71 | 72 | let null_rendered = handlebars.render_template(template, &Value::Null); 73 | assert!(null_rendered.is_ok()); 74 | assert_eq!(&null_rendered.unwrap(), ""); 75 | } 76 | 77 | pub(crate) fn test_error_against_configs( 78 | handlebars: &Handlebars, 79 | template: &str, 80 | expected: &str, 81 | ) { 82 | let config_rendered = handlebars.render_template(template, &config_fixture()); 83 | assert!(!config_rendered.is_ok()); 84 | assert_eq!(&config_rendered.unwrap_err().desc, expected); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/storage/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::app::datadogstatsd::CustomMetrics; 2 | use crate::app::datadogstatsd::DdMetrics; 3 | use anyhow::Result; 4 | use hogan::config::Environment; 5 | use hogan::config::EnvironmentDescription; 6 | use riker::actors::*; 7 | use std::sync::Arc; 8 | use std::time::{Duration, SystemTime}; 9 | 10 | pub trait Cache { 11 | fn id(&self) -> &str; 12 | fn clean(&self, max_age: usize) -> Result<()>; 13 | fn read_env(&self, env: &str, sha: &str) -> Result>>; 14 | fn write_env(&self, env: &str, sha: &str, data: &Environment) -> Result<()>; 15 | fn read_env_listing(&self, sha: &str) -> Result>>>; 16 | fn write_env_listing(&self, sha: &str, data: &[EnvironmentDescription]) -> Result<()>; 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct ExecuteCleanup {} 21 | 22 | type CacheBox = Arc>; 23 | 24 | #[actor(ExecuteCleanup)] 25 | pub struct CleanupActor { 26 | caches: Vec, 27 | max_age: usize, 28 | metrics: Arc, 29 | } 30 | 31 | impl ActorFactoryArgs<(Vec, usize, Arc)> for CleanupActor { 32 | fn create_args((caches, max_age, metrics): (Vec, usize, Arc)) -> Self { 33 | CleanupActor { 34 | caches, 35 | max_age, 36 | metrics, 37 | } 38 | } 39 | } 40 | 41 | impl Actor for CleanupActor { 42 | type Msg = CleanupActorMsg; 43 | 44 | fn recv(&mut self, ctx: &Context, msg: Self::Msg, sender: Sender) { 45 | self.receive(ctx, msg, sender); 46 | } 47 | } 48 | 49 | impl Receive for CleanupActor { 50 | type Msg = CleanupActorMsg; 51 | 52 | fn receive(&mut self, _ctx: &Context, _msg: ExecuteCleanup, _sender: Sender) { 53 | let now = SystemTime::now(); 54 | for cache in self.caches.iter() { 55 | match cache.clean(self.max_age) { 56 | Ok(()) => { 57 | let duration = now.elapsed().unwrap_or(Duration::from_millis(0)); 58 | info!( 59 | "Cleaned entries from the {} cache older than {} days. Time {} ms", 60 | cache.id(), 61 | self.max_age, 62 | duration.as_millis() 63 | ); 64 | self.metrics.time( 65 | CustomMetrics::DbCleanup.into(), 66 | None, 67 | duration.as_millis() as i64, 68 | ); 69 | } 70 | Err(err) => { 71 | error!("Unable to clean the {} cache: {:?}", cache.id(), err); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl CleanupActor { 79 | pub fn init_db_cleanup_system( 80 | system: &ActorSystem, 81 | caches: &[CacheBox], 82 | max_age: usize, 83 | metrics: Arc, 84 | ) { 85 | let cleanup_poller_delay = 24 * 60 * 60; //1 day 86 | let worker = system 87 | .actor_of_args::( 88 | "db-cleanup-worker", 89 | (caches.to_owned(), max_age, metrics), 90 | ) 91 | .unwrap(); 92 | 93 | system.schedule( 94 | Duration::from_secs(cleanup_poller_delay), 95 | Duration::from_secs(cleanup_poller_delay), 96 | worker.clone(), 97 | None, 98 | ExecuteCleanup {}, 99 | ); 100 | 101 | info!( 102 | "Scheduled db cleanup poller for every {} s", 103 | cleanup_poller_delay 104 | ); 105 | 106 | worker.tell(ExecuteCleanup {}, None); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Environment; 2 | use crate::error::HoganError; 3 | use crate::find_file_paths; 4 | use anyhow::{Context, Result}; 5 | use handlebars::Handlebars; 6 | use regex::Regex; 7 | use zip::write::{FileOptions, ZipWriter}; 8 | use zip::CompressionMethod::Stored; 9 | 10 | use std::clone::Clone; 11 | use std::fs; 12 | use std::io::{Cursor, Write}; 13 | use std::path::PathBuf; 14 | 15 | pub struct TemplateDir { 16 | directory: PathBuf, 17 | } 18 | 19 | impl TemplateDir { 20 | pub fn new(path: PathBuf) -> Result { 21 | if !path.is_dir() { 22 | Err(HoganError::UnknownError { 23 | msg: "Unable to find the template path".to_string(), 24 | }) 25 | .with_context(|| format!("The path {:?} needs to exist and be a directory", path)) 26 | } else { 27 | Ok(TemplateDir { directory: path }) 28 | } 29 | } 30 | 31 | pub fn find(&self, filter: Regex) -> Vec