diff --git a/components/sup/doc/render_context_schema.json b/components/sup/doc/render_context_schema.json index 1d3af33477..b921c8c3d7 100644 --- a/components/sup/doc/render_context_schema.json +++ b/components/sup/doc/render_context_schema.json @@ -498,8 +498,17 @@ "properties": { "first": { "description": "The first member of this service group. If the group is running in a leader topology, this will also be the leader.", + "$deprecated": "Since 0.56.0; if you want the leader, use `leader` explicitly. 'first' isn't deterministic, either, so you can just use `members[0]` instead", "$ref": "#/definitions/svc_member" }, + "leader": { + "description": "The current leader of this service group, if running in a leader topology", + "$since": "0.56.0", + "oneOf": [ + { "$ref": "#/definitions/svc_member" }, + { "type": "null" } + ] + }, "members": { "description": "All members of the service group, across the entire ring. Includes all liveness states!", "type": "array", @@ -510,6 +519,7 @@ }, "required": [ "first", + "leader", "members" ], "additionalProperties": false diff --git a/components/sup/src/templating/context.rs b/components/sup/src/templating/context.rs index 8debafad23..dbd0255b04 100644 --- a/components/sup/src/templating/context.rs +++ b/components/sup/src/templating/context.rs @@ -417,6 +417,7 @@ impl<'a> Binds<'a> { #[derive(Clone, Debug, Serialize)] struct BindGroup<'a> { first: Option>, + leader: Option>, members: Vec>, } @@ -424,6 +425,7 @@ impl<'a> BindGroup<'a> { fn new(group: &'a CensusGroup) -> Self { BindGroup { first: select_first(group), + leader: group.leader().map(|m| SvcMember::from_census_member(m)), members: group .members() .iter() @@ -610,20 +612,24 @@ fn select_first(census_group: &CensusGroup) -> Option { #[cfg(test)] mod tests { use super::*; - use std::io::Write; - use std::io::Read; - use json; - use serde_json; + + use std::collections::BTreeMap; use std::fs; - use valico::json_schema; + use std::io::{Read, Write}; + use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; + + use serde_json; + use json; use tempdir::TempDir; - use manager::service::config::PackageConfigPaths; - use std::net::IpAddr; - use std::net::Ipv4Addr; - use std::collections::BTreeMap; + use valico::json_schema; + + use butterfly::rumor::service::SysInfo; use hcore::package::PackageIdent; + use manager::service::Cfg; + use manager::service::config::PackageConfigPaths; + use templating::TemplateRenderer; /// Asserts that `json_string` is valid according to our render /// context JSON schema. @@ -671,7 +677,8 @@ JSON: serde_json::from_str(&raw_schema).expect("Could not parse schema as JSON"); let mut scope = json_schema::scope::Scope::new(); // NOTE: using `false` instead of `true` allows us to use - // `$comment` keys + // `$comment` keyword, as well as our own `$deprecated` and + // `$since` keywords. let schema = scope.compile_and_return(parsed_schema, false).expect( "Could not compile the schema", ); @@ -745,6 +752,37 @@ two = 2 //////////////////////////////////////////////////////////////////////// + /// Create a basic SvcMember struct for use in tests + fn default_svc_member<'a>() -> SvcMember<'a> { + let ident = PackageIdent::new("core", "test_pkg", Some("1.0.0"), Some("20180321150416")); + SvcMember { + member_id: Cow::Owned("MEMBER_ID".into()), + pkg: Cow::Owned(Some(ident)), + application: Cow::Owned(None), + environment: Cow::Owned(None), + service: Cow::Owned("foo".into()), + group: Cow::Owned("default".into()), + org: Cow::Owned(None), + persistent: Cow::Owned(true), + leader: Cow::Owned(false), + follower: Cow::Owned(false), + update_leader: Cow::Owned(false), + update_follower: Cow::Owned(false), + election_is_running: Cow::Owned(false), + election_is_no_quorum: Cow::Owned(false), + election_is_finished: Cow::Owned(false), + update_election_is_running: Cow::Owned(false), + update_election_is_no_quorum: Cow::Owned(false), + update_election_is_finished: Cow::Owned(false), + sys: Cow::Owned(SysInfo::new()), + alive: Cow::Owned(true), + suspect: Cow::Owned(false), + confirmed: Cow::Owned(false), + departed: Cow::Owned(false), + cfg: Cow::Owned(BTreeMap::new() as toml::value::Table), + } + } + /// Just create a basic RenderContext that could be used in tests. /// /// If you want to modify parts of it, it's easier to change @@ -814,39 +852,13 @@ two = 2 let (_tmp_dir, test_pkg) = new_test_pkg(); let cfg = Cfg::new(&test_pkg, None).expect("create config"); - use butterfly::rumor::service::SysInfo; - let sys_info = SysInfo::new(); - // TODO (CM): just create a toml table directly let mut svc_member_cfg = BTreeMap::new(); svc_member_cfg.insert("foo".into(), "bar".into()); - let me = SvcMember { - member_id: Cow::Owned("MEMBER_ID".into()), - pkg: Cow::Owned(Some(ident.clone())), - application: Cow::Owned(None), - environment: Cow::Owned(None), - service: Cow::Owned("foo".into()), - group: Cow::Owned("default".into()), - org: Cow::Owned(None), - persistent: Cow::Owned(true), - leader: Cow::Owned(false), - follower: Cow::Owned(false), - update_leader: Cow::Owned(false), - update_follower: Cow::Owned(false), - election_is_running: Cow::Owned(false), - election_is_no_quorum: Cow::Owned(false), - election_is_finished: Cow::Owned(false), - update_election_is_running: Cow::Owned(false), - update_election_is_no_quorum: Cow::Owned(false), - update_election_is_finished: Cow::Owned(false), - sys: Cow::Owned(sys_info), - alive: Cow::Owned(true), - suspect: Cow::Owned(false), - confirmed: Cow::Owned(false), - departed: Cow::Owned(false), - cfg: Cow::Owned(svc_member_cfg as toml::value::Table), - }; + let mut me = default_svc_member(); + me.pkg = Cow::Owned(Some(ident.clone())); + me.cfg = Cow::Owned(svc_member_cfg as toml::value::Table); let svc = Svc { service_group: Cow::Owned(group), @@ -862,6 +874,7 @@ two = 2 let mut bind_map = HashMap::new(); let bind_group = BindGroup { first: Some(me.clone()), + leader: None, members: vec![me.clone()], }; bind_map.insert("foo".into(), bind_group); @@ -876,6 +889,20 @@ two = 2 } } + /// Render the given template string using the given context, + /// returning the result. This can help to verify that + /// RenderContext data are accessible to users in the way we + /// expect. + fn render(template_content: &str, ctx: &RenderContext) -> String { + let mut renderer = TemplateRenderer::new(); + renderer + .register_template_string("testing", template_content) + .expect("Could not register template content"); + renderer.render("testing", ctx).expect( + "Could not render template", + ) + } + //////////////////////////////////////////////////////////////////////// /// Reads a file containing real rendering context output from an @@ -923,4 +950,47 @@ two = 2 assert_valid(&j); } + #[test] + fn no_leader_renders_correctly() { + let ctx = default_render_context(); + + // Just make sure our default context is set up how this test + // is expecting + assert!(ctx.bind.0.get("foo").unwrap().leader.is_none()); + + let output = render( + "{{#if bind.foo.leader}}THERE IS A LEADER{{else}}NO LEADER{{/if}}", + &ctx, + ); + + assert_eq!(output, "NO LEADER"); + } + + #[test] + fn leader_renders_correctly() { + let mut ctx = default_render_context(); + + // Let's create a new leader, with a custom member_id + let mut svc_member = default_svc_member(); + svc_member.member_id = Cow::Owned("deadbeefdeadbeefdeadbeefdeadbeef".into()); + + // Set up our own bind with a leader + let mut bind_map = HashMap::new(); + let bind_group = BindGroup { + first: Some(svc_member.clone()), + leader: Some(svc_member.clone()), + members: vec![svc_member.clone()], + }; + bind_map.insert("foo".into(), bind_group); + let binds = Binds(bind_map); + ctx.bind = binds; + + // This template should reveal the member_id of the leader + let output = render( + "{{#if bind.foo.leader}}{{bind.foo.leader.member_id}}{{else}}NO LEADER{{/if}}", + &ctx, + ); + + assert_eq!(output, "deadbeefdeadbeefdeadbeefdeadbeef"); + } } diff --git a/components/sup/tests/fixtures/sample_render_context.json b/components/sup/tests/fixtures/sample_render_context.json index 5671bfe67a..47e213fa72 100644 --- a/components/sup/tests/fixtures/sample_render_context.json +++ b/components/sup/tests/fixtures/sample_render_context.json @@ -388,6 +388,7 @@ "update_follower": false, "update_leader": false }, + "leader": null, "members": [ { "alive": true,