Skip to main content

isb_core/stack/controller/
dns.rs

1//! Service names ([`crate::discovery`]): each worker publishes its
2//! in-rotation replicas as the service's name, and also in the stack's
3//! project environment when the [`DnsScopeFn`] says so.
4
5use std::collections::BTreeSet;
6use std::net::IpAddr;
7use std::sync::Arc;
8
9use super::{Controller, StackDef, Worker};
10use crate::org::OrgId;
11
12/// The project environment a stack's services are also named in (see
13/// [`crate::discovery`]).
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct DnsScope {
16    /// `<project>-<env>`: what the extra names use in place of the stack.
17    pub name: String,
18    /// The services that get them: a service whose name another one in
19    /// the environment holds gets none.
20    pub alias: BTreeSet<String>,
21}
22
23/// Which scope, if any, a stack (by org and own name) has. Called from
24/// worker threads on every pass, never with a controller lock held, so it
25/// must answer from memory.
26pub type DnsScopeFn = Arc<dyn Fn(&OrgId, &str) -> Option<DnsScope> + Send + Sync>;
27
28/// Drop the names of services no stored stack has (stacks removed while no
29/// daemon ran).
30pub(super) fn prune(defs: &[StackDef]) {
31    for def in defs {
32        let dir = crate::discovery::org_dir(&def.org);
33        let keep: Vec<(String, String)> = defs
34            .iter()
35            .filter(|d| d.org == def.org)
36            .flat_map(|d| d.file.services.keys().map(|s| (d.name.clone(), s.clone())))
37            .collect();
38        crate::discovery::prune(&dir, &keep);
39    }
40}
41
42impl Controller {
43    /// Name services in their stack's project environment too, as `f`
44    /// says (the apps' compose ownership). Each worker asks on its next
45    /// pass and rewrites its hosts file when the answer changed.
46    pub fn set_dns_scope(&self, f: DnsScopeFn) {
47        *self.inner.dns_scope.lock().unwrap() = Some(f);
48    }
49
50    /// Something [`Controller::set_dns_scope`]'s answer depends on changed
51    /// in `org` (a stack joined or left a project environment): wake the
52    /// org's workers, so their names follow now rather than on their next
53    /// pass.
54    pub fn republish_dns(&self, org: &OrgId) {
55        let stacks = self.inner.stacks.lock().unwrap();
56        let ours: BTreeSet<String> = stacks
57            .iter()
58            .filter(|(_, d)| d.org == *org)
59            .map(|(q, _)| q.clone())
60            .collect();
61        drop(stacks);
62        let workers = self.inner.workers.lock().unwrap();
63        for ((q, _), w) in workers.iter() {
64            if ours.contains(q) {
65                let _slot = w.slot.lock().unwrap();
66                w.wake.notify_all();
67            }
68        }
69    }
70}
71
72impl Worker {
73    /// Publish the in-rotation replicas' addresses as the service's name
74    /// (see [`crate::discovery`]). Nothing to do in an org created without
75    /// service names.
76    pub(super) fn sync_dns(&mut self) {
77        let dir = crate::discovery::org_dir(&self.org);
78        self.sync_dns_in(&dir);
79    }
80
81    /// The project environment this service is also named in, if any.
82    fn dns_alias(&self) -> Option<String> {
83        let f = self.inner.dns_scope.lock().unwrap().clone()?;
84        let s = f(&self.org, &self.stack)?;
85        (s.name != self.stack && s.alias.contains(&self.service)).then_some(s.name)
86    }
87
88    pub(super) fn sync_dns_in(&mut self, dir: &std::path::Path) {
89        let mut ips: Vec<IpAddr> = self
90            .rt
91            .values()
92            .filter(|r| r.in_rotation)
93            .filter_map(|r| r.ip)
94            .collect();
95        ips.sort();
96        // A worker that has published nothing yet (a daemon restart) leaves
97        // the last records alone until a replica is back in rotation, rather
98        // than blanking the name while health is being re-established.
99        let fresh = self.dns_last.is_none() && ips.is_empty();
100        if fresh || !dir.is_dir() {
101            return;
102        }
103        let want = (ips, self.dns_alias());
104        if self.dns_last.as_ref() == Some(&want) {
105            return;
106        }
107        let (ips, alias) = &want;
108        match crate::discovery::publish(
109            dir,
110            &self.org,
111            &self.stack,
112            &self.service,
113            ips,
114            alias.as_deref(),
115        ) {
116            Ok(()) => {
117                self.dns_last = Some(want);
118                self.dns_error = None;
119            }
120            Err(e) => {
121                let e = e.to_string();
122                if self.dns_error.as_deref() != Some(&e) {
123                    self.event(
124                        "warn",
125                        None,
126                        &format!("cannot publish the service name: {e}"),
127                    );
128                    self.dns_error = Some(e);
129                }
130            }
131        }
132    }
133
134    /// Take the service's name away, whoever published it.
135    pub(super) fn unpublish_dns(&mut self) {
136        if let Some(dir) = Some(crate::discovery::org_dir(&self.org)).filter(|d| d.is_dir()) {
137            if let Err(e) =
138                crate::discovery::publish(&dir, &self.org, &self.stack, &self.service, &[], None)
139            {
140                self.log(&format!("cannot remove the service name: {e}"));
141            }
142        }
143        self.dns_last = Some((Vec::new(), None));
144    }
145}