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 ade7a522b4..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() @@ -627,6 +629,7 @@ mod tests { 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. @@ -674,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", ); @@ -870,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); @@ -884,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 @@ -931,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,