186 lines
7.5 KiB
Rust
186 lines
7.5 KiB
Rust
impl App {
|
|
pub fn start_dns_acl_updates(self: &Arc<Self>) {
|
|
self.tunnels.spawn(crate::acl::dynamic::poll_loop(
|
|
Arc::downgrade(self), self.shutdown.clone(),
|
|
));
|
|
}
|
|
|
|
pub fn start_geoip_updates(self: &Arc<Self>) {
|
|
self.tunnels.spawn(crate::geoip::update_poll_loop(
|
|
Arc::downgrade(self),
|
|
self.shutdown.clone(),
|
|
));
|
|
}
|
|
|
|
pub fn start_update_check(self: &Arc<Self>, config: crate::update_check::Config) {
|
|
self.software_update.configure(&config);
|
|
self.tunnels.spawn(crate::update_check::poll_loop(
|
|
self.software_update.clone(),
|
|
config,
|
|
self.shutdown.clone(),
|
|
));
|
|
}
|
|
|
|
/// Drain detached writers after the listeners and their handlers have stopped.
|
|
pub async fn drain_updates(&self) {
|
|
let _all_slots = self.update_slots.clone().acquire_many_owned(8).await;
|
|
}
|
|
/// The detached task completes persistence + publication even if the HTTP caller disconnects.
|
|
pub async fn update(self: &Arc<Self>, update: Update) -> Result<config::Envelope, ApiError> {
|
|
self.update_as(update, "local".into()).await
|
|
}
|
|
pub async fn update_as(
|
|
self: &Arc<Self>,
|
|
update: Update,
|
|
actor: String,
|
|
) -> Result<config::Envelope, ApiError> {
|
|
self.update_as_action(update, actor, None).await
|
|
}
|
|
pub async fn restore_as(
|
|
self: &Arc<Self>,
|
|
update: Update,
|
|
actor: String,
|
|
source_revision: u64,
|
|
) -> Result<config::Envelope, ApiError> {
|
|
self.update_as_action(update, actor, Some(source_revision))
|
|
.await
|
|
}
|
|
async fn update_as_action(
|
|
self: &Arc<Self>,
|
|
update: Update,
|
|
actor: String,
|
|
source_revision: Option<u64>,
|
|
) -> Result<config::Envelope, ApiError> {
|
|
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
|
|
ApiError(
|
|
StatusCode::TOO_MANY_REQUESTS,
|
|
"configuration update queue is full".into(),
|
|
)
|
|
})?;
|
|
let expected_revision = update.expected_revision;
|
|
let revision = expected_revision
|
|
.checked_add(1)
|
|
.ok_or_else(|| ApiError(StatusCode::BAD_REQUEST, "revision overflow".into()))?;
|
|
let policy = self.policy.clone();
|
|
let defaults = self.startup_listeners;
|
|
let previous = self.config.load_full();
|
|
let previous_config = previous.envelope.config.clone();
|
|
let active_listeners = self.active_listeners.clone();
|
|
let observed = self.docker.snapshot().await;
|
|
let desired_vhost_count = update.config.vhosts.len();
|
|
let desired_config = update.config;
|
|
self.startup_locks
|
|
.validate(&desired_config, self.policy.admin_addr)
|
|
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
|
|
crate::tls::validate_certificate_removals(&previous_config, &desired_config)
|
|
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
|
|
let (prepared, invalidation, slot) = tokio::task::spawn_blocking(move || {
|
|
let effective = crate::docker::effective_config(desired_config, &observed)?;
|
|
let mut prepared = config::compile_with_active(
|
|
effective,
|
|
revision,
|
|
&policy,
|
|
defaults,
|
|
Some(&previous),
|
|
&active_listeners,
|
|
)?;
|
|
prepared
|
|
.envelope
|
|
.config
|
|
.vhosts
|
|
.truncate(desired_vhost_count);
|
|
let invalidation = cache_invalidation(&previous_config, &prepared.envelope.config);
|
|
Ok::<_, anyhow::Error>((prepared, invalidation, slot))
|
|
})
|
|
.await
|
|
.map_err(|_| {
|
|
ApiError(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"configuration validation task failed".into(),
|
|
)
|
|
})?
|
|
.map_err(|e| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{e:#}")))?;
|
|
let this = self.clone();
|
|
tokio::spawn(async move {
|
|
let _slot = slot;
|
|
let _lock = this.update_lock.lock().await;
|
|
if this.config.load().envelope.revision != expected_revision {
|
|
return Err(ApiError(
|
|
StatusCode::CONFLICT,
|
|
"configuration changed; reload before saving".into(),
|
|
));
|
|
}
|
|
let persisted = match source_revision {
|
|
Some(source) => {
|
|
this.store
|
|
.replace_as_restore(
|
|
expected_revision,
|
|
&prepared.envelope.config,
|
|
&actor,
|
|
source,
|
|
)
|
|
.await
|
|
}
|
|
None => {
|
|
this.store
|
|
.replace_as(expected_revision, &prepared.envelope.config, &actor)
|
|
.await
|
|
}
|
|
};
|
|
match persisted {
|
|
Ok(true) => {}
|
|
Ok(false) => {
|
|
return Err(ApiError(
|
|
StatusCode::CONFLICT,
|
|
"stored revision differs; external SQLite writes are unsupported".into(),
|
|
))
|
|
}
|
|
Err(error) => {
|
|
tracing::error!(%error, "configuration persistence failed");
|
|
return Err(ApiError(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"configuration was not saved".into(),
|
|
));
|
|
}
|
|
}
|
|
// No await between successful commit and publication. Cache identity is
|
|
// intentionally decoupled from the configuration revision, so unrelated
|
|
// control-plane saves do not invalidate hot/cold objects.
|
|
let envelope = prepared.envelope.clone();
|
|
// After commit, reject late cache tickets from the old routing
|
|
// snapshot before any scope purge. Existing unaffected objects stay.
|
|
this.cache.set_control_revision(envelope.revision);
|
|
match invalidation {
|
|
CacheInvalidation::All(cache_revision) => {
|
|
this.cache.purge(Some(cache_revision));
|
|
}
|
|
CacheInvalidation::Scopes(scopes) => {
|
|
for scope in scopes {
|
|
this.cache.purge_scope(&scope);
|
|
}
|
|
}
|
|
CacheInvalidation::None => {}
|
|
}
|
|
// A COLD entry is usable after restart only if its scope still has
|
|
// the same origin/content policy. Publish the fingerprints after
|
|
// the cache generation barriers but before the routing snapshot.
|
|
this.cache.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
|
|
this.cache.configure(envelope.config.cache.clone());
|
|
this.rate_limiter.reset();
|
|
this.logs.configure(envelope.config.logging.clone());
|
|
crate::logging::configure_error_log(envelope.config.error_log.clone());
|
|
this.config.store(Arc::new(prepared));
|
|
crate::acl::dynamic::config_changed();
|
|
tracing::info!(revision, "configuration applied");
|
|
Ok(envelope)
|
|
})
|
|
.await
|
|
.map_err(|_| {
|
|
ApiError(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"configuration task failed".into(),
|
|
)
|
|
})?
|
|
}
|
|
}
|