Skip to main content

isb_core/ingress/
status.rs

1//! What the ingress reports: the `ingress_status` tool's view, and each
2//! route's [`DomainStatus`].
3
4use super::*;
5
6impl Manager {
7    /// Everything the `ingress_status` tool shows, for the given orgs (all
8    /// when `None`).
9    pub fn status(&self, orgs: Option<&[OrgId]>) -> Value {
10        let sees = |o: &OrgId| orgs.is_none_or(|v| v.contains(o));
11        let st = self.state.lock().unwrap();
12        let mut routes = Vec::new();
13        for s in st.served.iter().filter(|s| sees(&s.route.org)) {
14            let ds = self.domain_status_of(&st, s);
15            routes.push(json!({
16                "org": s.route.org,
17                "stack": s.route.stack,
18                "service": s.route.service,
19                "domain": ds,
20            }));
21        }
22        let conflicts: Vec<Value> = st
23            .conflicts
24            .iter()
25            .filter(|c| sees(&c.route.org))
26            .map(|c| {
27                json!({
28                    "org": c.route.org, "stack": c.route.stack, "service": c.route.service,
29                    "host": c.route.host, "path": c.route.path, "reason": c.reason,
30                })
31            })
32            .collect();
33        let mut refused = Vec::new();
34        for ((q, svc), list) in &st.refused {
35            let org = crate::stack::split_qualified(q)
36                .map(|(o, _)| o)
37                .unwrap_or_else(|_| OrgId::default_org());
38            if !sees(&org) {
39                continue;
40            }
41            for (h, p, why) in list {
42                refused
43                    .push(json!({"stack": q, "service": svc, "host": h, "path": p, "reason": why}));
44            }
45        }
46        let tunnels: Vec<&TunnelStatus> = st
47            .tunnels
48            .iter()
49            .filter(|(o, _)| sees(o))
50            .map(|(_, t)| t)
51            .collect();
52        json!({
53            "enabled": true,
54            "http": self.cfg.http.map(|a| a.to_string()),
55            "https": self.cfg.https.map(|a| a.to_string()),
56            "ca": match &self.cfg.ca { Ca::Internal => "internal".to_string(), Ca::Acme(u) => u.clone() },
57            "public_ip": self.cfg.public_ip.map(|a| a.to_string()),
58            "tunnel_port": self.cfg.tunnel_port,
59            "caddy": self.edge.get().map(|e| serde_json::to_value(e.status()).unwrap_or_default()),
60            "error": st.last_error,
61            "routes": routes,
62            "conflicts": conflicts,
63            "refused": refused,
64            "tunnels": tunnels,
65        })
66    }
67
68    pub(super) fn domain_status_of(&self, st: &State, s: &Served) -> DomainStatus {
69        let r = &s.route;
70        let provider = match s.via {
71            Via::Public => crate::org::INGRESS_CADDY,
72            Via::Tunnel(_) => crate::org::INGRESS_CLOUDFLARE_TUNNEL,
73        };
74        let mut d = DomainStatus {
75            host: r.host.clone(),
76            path: r.path.clone(),
77            url: None,
78            https: r.https,
79            provider: provider.into(),
80            state: String::new(),
81            cert: String::new(),
82            message: None,
83            upstreams: s.upstreams.iter().map(|a| a.to_string()).collect(),
84        };
85        let off = match s.via {
86            Via::Tunnel(_) => {
87                d.cert = if r.https { "cloudflare" } else { "none" }.into();
88                d.url = Some(r.url(None, None));
89                None
90            }
91            Via::Public if r.https => {
92                d.url = Some(r.url(self.cfg.https.map(|a| a.port()), None));
93                if self.cfg.https.is_none() {
94                    d.cert = "none".into();
95                    Some("this server has no HTTPS listener (isb serve --ingress-https)")
96                } else if caddy::needs_dns_challenge(&r.host, &self.cfg.ca) {
97                    d.cert = "unsupported".into();
98                    d.message = Some("a wildcard certificate needs a DNS challenge, which this ingress cannot do; use the cloudflare-tunnel provider".into());
99                    None
100                } else {
101                    let key = self.cfg.ca.issuer_key();
102                    match st.certs.get(&r.host) {
103                        Some(c) if c.state == "failed" => {
104                            d.cert = "failed".into();
105                            d.message = c.error.clone();
106                        }
107                        Some(c) if c.state == "issued" => d.cert = "issued".into(),
108                        _ if caddy::cert_in_storage(&self.storage(), &key, &r.host) => {
109                            d.cert = "issued".into()
110                        }
111                        _ => d.cert = "pending".into(),
112                    }
113                    None
114                }
115            }
116            Via::Public => {
117                d.cert = "none".into();
118                d.url = Some(r.url(None, self.cfg.http.map(|a| a.port())));
119                if self.cfg.http.is_none() {
120                    Some("this server has no HTTP listener (isb serve --ingress-http)")
121                } else {
122                    None
123                }
124            }
125        };
126        d.state = if let Some(why) = off {
127            d.message = Some(why.into());
128            "off".into()
129        } else if r.redirect.is_some() {
130            "redirect".into()
131        } else if s.upstreams.is_empty() {
132            "no-replicas".into()
133        } else {
134            "serving".into()
135        };
136        d
137    }
138}