externaldns_webhook/
webhook.rs

1use crate::{
2    IDoNotCareWhich, MEDIATYPE, changes::Changes, domain_filter::DomainFilter, endpoint::Endpoint,
3    provider::Provider, status::Status, webhook_json::WebhookJson,
4};
5use actix_web::{
6    App, HttpServer, get,
7    guard::GuardContext,
8    http::{StatusCode, header::Accept},
9    post,
10    web::{Data, Json},
11};
12use logcall::logcall;
13use std::sync::Arc;
14
15/// Setup of the HTTP server
16/// The listening addresses and ports are specified in ExternalDNS,
17/// hence they are not exposed to be configurable.
18#[derive(Debug)]
19pub struct Webhook {
20    provider_address: String,
21    provider_port: u16,
22    dns_manager: Arc<dyn Provider>,
23
24    exposed_address: String,
25    exposed_port: u16,
26    status: Arc<dyn Status>,
27}
28impl Webhook {
29    /// Constructor of `Webhook`.
30    #[logcall("debug")]
31    pub fn new(dns_manager: Arc<dyn Provider>, status: Arc<dyn Status>) -> Webhook {
32        // As much as the http values are customizable, those are the value asked in ExternalDNS doc.
33        Webhook {
34            provider_address: "127.0.0.1".to_string(),
35            provider_port: 8888,
36            dns_manager,
37            exposed_address: "0.0.0.0".to_string(),
38            exposed_port: 8080,
39            status,
40        }
41    }
42
43    /// Start the webhook server, and healthz web server.
44    #[logcall(ok = "debug", err = "error")]
45    pub async fn start(&self) -> anyhow::Result<()> {
46        let x = self.status.clone();
47        let exposed = HttpServer::new(move || {
48            App::new()
49                .app_data(Data::new(x.clone()))
50                .service(get_healthz)
51                .service(get_metrics)
52        })
53        .bind((self.exposed_address.clone(), self.exposed_port))?
54        .run();
55
56        let x = self.dns_manager.clone();
57        let provider = HttpServer::new(move || {
58            App::new()
59                .app_data(Data::new(x.clone()))
60                .service(get_root)
61                .service(get_records)
62                .service(post_records)
63                .service(post_adjustendpoints)
64        })
65        .bind((self.provider_address.clone(), self.provider_port))?
66        .run();
67
68        tokio::spawn(exposed);
69        provider.await?;
70
71        Ok(())
72    }
73}
74
75// Negotiate `DomainFilter`
76#[logcall("debug")]
77#[get("/", guard = "media_type_guard")]
78async fn get_root(dns_manager: Data<Arc<dyn Provider>>) -> WebhookJson<DomainFilter> {
79    WebhookJson(Json(dns_manager.domain_filter().await))
80}
81
82// Get records
83#[logcall("debug")]
84#[get("/records", guard = "media_type_guard")]
85async fn get_records(dns_manager: Data<Arc<dyn Provider>>) -> WebhookJson<Vec<Endpoint>> {
86    WebhookJson(Json(dns_manager.records().await))
87}
88
89// Apply record
90#[logcall("debug")]
91#[post("/records")]
92async fn post_records(
93    dns_manager: Data<Arc<dyn Provider>>,
94    changes: Json<Changes>,
95) -> (String, StatusCode) {
96    match dns_manager.apply_changes(changes.0).await {
97        Ok(_) => ("".to_string(), StatusCode::OK),
98        Err(e) => (format!("{e:?}"), StatusCode::INTERNAL_SERVER_ERROR),
99    }
100}
101
102// Provider specific adjustments of records
103#[logcall("debug")]
104#[post("/adjustendpoints", guard = "media_type_guard")]
105async fn post_adjustendpoints(
106    dns_manager: Data<Arc<dyn Provider>>,
107    endpoints: Json<Vec<Endpoint>>,
108) -> (Json<IDoNotCareWhich<Vec<Endpoint>, String>>, StatusCode) {
109    match dns_manager.adjust_endpoints(endpoints.0).await {
110        Ok(x) => (Json(IDoNotCareWhich::One(x)), StatusCode::OK),
111        Err(e) => (
112            Json(IDoNotCareWhich::Another(format!("{e:?}"))),
113            StatusCode::INTERNAL_SERVER_ERROR,
114        ),
115    }
116}
117
118// Only takes and gives `MEDIATYPE`, why guard.
119fn media_type_guard(ctx: &GuardContext<'_>) -> bool {
120    ctx.header::<Accept>()
121        .map_or(false, |h| h.preference() == MEDIATYPE)
122}
123
124// #[logcall("debug")]
125#[get("/healthz")]
126async fn get_healthz(status: Data<Arc<dyn Status>>) -> (String, StatusCode) {
127    status.healthz().await
128}
129
130// #[logcall("debug")]
131#[get("/metrics")]
132async fn get_metrics(status: Data<Arc<dyn Status>>) -> (String, StatusCode) {
133    status.metrics().await
134}