diff --git a/Cargo.lock b/Cargo.lock index 653e2d8..0c95748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,21 @@ version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +[[package]] +name = "cfcache" +version = "0.1.0" +dependencies = [ + "eyre", + "kankyo", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-futures", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -1068,12 +1083,35 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.6", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + [[package]] name = "miow" version = "0.2.2" @@ -1086,6 +1124,16 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi 0.3.9", +] + [[package]] name = "multipart" version = "0.17.1" @@ -1913,6 +1961,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + [[package]] name = "sitemap" version = "0.4.1" @@ -2069,12 +2126,17 @@ dependencies = [ "futures-core", "iovec", "lazy_static", + "libc", "memchr", "mio", + "mio-named-pipes", + "mio-uds", "num_cpus", "pin-project-lite 0.1.11", + "signal-hook-registry", "slab", "tokio-macros", + "winapi 0.3.9", ] [[package]] @@ -2603,6 +2665,7 @@ checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" name = "xesite" version = "2.2.0" dependencies = [ + "cfcache", "chrono", "color-eyre", "comrak", @@ -2621,7 +2684,7 @@ dependencies = [ "pfacts", "pretty_env_logger", "prometheus", - "rand 0.7.3", + "rand 0.8.1", "reqwest", "ructe", "sdnotify", diff --git a/Cargo.toml b/Cargo.toml index f398181..03afd0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ log = "0.4" mime = "0.3.0" prometheus = { version = "0.10", default-features = false, features = ["process"] } rand = "0" +reqwest = { version = "0.10", features = ["json"] } sdnotify = { version = "0.1", default-features = false } serde_dhall = "0.8.0" serde = { version = "1", features = ["derive"] } @@ -37,6 +38,7 @@ url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } # workspace dependencies +cfcache = { path = "./lib/cfcache" } go_vanity = { path = "./lib/go_vanity" } jsonfeed = { path = "./lib/jsonfeed" } mi = { path = "./lib/mi" } diff --git a/lib/cfcache/Cargo.toml b/lib/cfcache/Cargo.toml new file mode 100644 index 0000000..85060c2 --- /dev/null +++ b/lib/cfcache/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cfcache" +version = "0.1.0" +authors = ["Christine Dodrill "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.10", features = ["json"] } +serde_json = "1.0" +serde = { version = "1", features = ["derive"] } +thiserror = "1" +tracing = "0.1" +tracing-futures = "0.2" + +[dev-dependencies] +eyre = "0.6.5" +kankyo = "0.3" +tokio = { version = "0.2", features = ["full"] } diff --git a/lib/cfcache/examples/purge.rs b/lib/cfcache/examples/purge.rs new file mode 100644 index 0000000..22c81a4 --- /dev/null +++ b/lib/cfcache/examples/purge.rs @@ -0,0 +1,15 @@ +use eyre::Result; + +#[tokio::main] +async fn main() -> Result<()> { + kankyo::init()?; + + let key = std::env::var("CF_TOKEN")?; + let zone_id = std::env::var("CF_ZONE_ID")?; + + let cli = cfcache::Client::new(key, zone_id)?; + cli.purge(vec!["https://christine.website/.within/health".to_string()]) + .await?; + + Ok(()) +} diff --git a/lib/cfcache/src/lib.rs b/lib/cfcache/src/lib.rs new file mode 100644 index 0000000..baf6775 --- /dev/null +++ b/lib/cfcache/src/lib.rs @@ -0,0 +1,64 @@ +use reqwest::header; +use tracing::instrument; + +pub type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + + #[error("request error: {0}")] + Request(#[from] reqwest::Error), + + #[error("invalid header value: {0}")] + InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), +} + +pub struct Client { + zone_id: String, + cli: reqwest::Client, +} + +static USER_AGENT: &str = concat!( + "xesite ", + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") +); + +impl Client { + pub fn new(api_key: String, zone_id: String) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Bearer {}", api_key))?, + ); + + let cli = reqwest::Client::builder() + .user_agent(USER_AGENT) + .default_headers(headers) + .build()?; + + Ok(Self { zone_id, cli }) + } + + #[instrument(skip(self), err)] + pub async fn purge(&self, urls: Vec) -> Result { + #[derive(serde::Serialize)] + struct Files { + files: Vec, + } + + self.cli + .post(&format!( + "https://api.cloudflare.com/client/v4/zones/{}/purge_cache", + self.zone_id + )) + .json(&Files { files: urls }) + .send() + .await? + .error_for_status()?; + Ok(()) + } +} diff --git a/lib/mi/src/lib.rs b/lib/mi/src/lib.rs index ec9d459..0e19bec 100644 --- a/lib/mi/src/lib.rs +++ b/lib/mi/src/lib.rs @@ -34,7 +34,7 @@ impl Client { }) } - #[instrument(skip(self))] + #[instrument(skip(self), err)] pub async fn mentioners(&self, url: String) -> Result> { Ok(self .cli @@ -46,6 +46,16 @@ impl Client { .json() .await?) } + + #[instrument(skip(self), err)] + pub async fn refresh(&self) -> Result<()> { + self.cli + .post("https://mi.within.website/api/blog/refresh") + .send() + .await? + .error_for_status()?; + Ok(()) + } } #[derive(Debug, Deserialize, Eq, PartialEq, Clone)] diff --git a/src/app/mod.rs b/src/app/mod.rs index e763792..6c01b2f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5,6 +5,7 @@ use std::{fs, path::PathBuf}; use tracing::{error, instrument}; pub mod markdown; +pub mod poke; #[derive(Clone, Deserialize)] pub struct Config { diff --git a/src/app/poke.rs b/src/app/poke.rs new file mode 100644 index 0000000..dd6f9fe --- /dev/null +++ b/src/app/poke.rs @@ -0,0 +1,86 @@ +use color_eyre::eyre::Result; +use std::{env, time::Duration}; +use tokio::time::delay_for; + +#[instrument(err)] +pub async fn the_cloud() -> Result<()> { + info!("waiting for things to settle"); + delay_for(Duration::from_secs(10)).await; + + info!("purging cloudflare cache"); + cloudflare().await?; + + info!("waiting for the cloudflare cache to purge globally"); + delay_for(Duration::from_secs(45)).await; + + info!("poking mi"); + mi().await?; + + info!("poking bing"); + bing().await?; + + info!("poking google"); + google().await?; + + Ok(()) +} + +#[instrument(err)] +async fn bing() -> Result<()> { + let cli = reqwest::Client::new(); + cli.get("https://www.bing.com/ping") + .query(&[("sitemap", "https://christine.website/sitemap.xml")]) + .header("User-Agent", crate::APPLICATION_NAME) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +#[instrument(err)] +async fn google() -> Result<()> { + let cli = reqwest::Client::new(); + cli.get("https://www.google.com/ping") + .query(&[("sitemap", "https://christine.website/sitemap.xml")]) + .header("User-Agent", crate::APPLICATION_NAME) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +#[instrument(err)] +async fn cloudflare() -> Result<()> { + let cli = cfcache::Client::new(env::var("CF_TOKEN")?, env::var("CF_ZONE_ID")?)?; + cli.purge( + vec![ + "https://christine.website/sitemap.xml", + "https://christine.website", + "https://christine.website/blog", + "https://christine.website/blog.atom", + "https://christine.website/blog.json", + "https://christine.website/blog.rss", + "https://christine.website/gallery", + "https://christine.website/talks", + "https://christine.website/resume", + "https://christine.website/signalboost", + "https://christine.website/feeds", + ] + .into_iter() + .map(|i| i.to_string()) + .collect(), + ) + .await?; + + Ok(()) +} + +#[instrument(err)] +async fn mi() -> Result<()> { + let cli = mi::Client::new(env::var("MI_TOKEN")?, crate::APPLICATION_NAME.to_string())?; + cli.refresh().await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 285bb93..91cd12b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,21 +39,6 @@ async fn main() -> Result<()> { .await?, ); - match sdnotify::SdNotify::from_env() { - Ok(ref mut n) => { - n.notify_ready().map_err(|why| { - error!("can't signal readiness to systemd: {}", why); - why - })?; - n.set_status(format!("hosting {} posts", state.clone().everything.len())) - .map_err(|why| { - error!("can't signal status to systemd: {}", why); - why - })?; - } - Err(why) => error!("not running under systemd with Type=notify: {}", why), - } - let healthcheck = warp::get().and(warp::path(".within").and(warp::path("health")).map(|| "OK")); let base = warp::path!("blog" / ..); @@ -222,6 +207,28 @@ async fn main() -> Result<()> { .with(warp::log(APPLICATION_NAME)) .recover(handlers::rejection); + match sdnotify::SdNotify::from_env() { + Ok(ref mut n) => { + // shitty heuristic for detecting if we're running in prod + tokio::spawn(async { + if let Err(why) = app::poke::the_cloud().await { + error!("Unable to poke the cloud: {}", why); + } + }); + + n.notify_ready().map_err(|why| { + error!("can't signal readiness to systemd: {}", why); + why + })?; + n.set_status(format!("hosting {} posts", state.clone().everything.len())) + .map_err(|why| { + error!("can't signal status to systemd: {}", why); + why + })?; + } + Err(why) => error!("not running under systemd with Type=notify: {}", why), + } + warp::serve(site) .run(( [0, 0, 0, 0],