use std::collections::BTreeSet;
use std::path::Path;
use std::sync::Arc;
use serde_json::{Value, json};
use crate::config::{Config, PolicyCommand};
use crate::context::ToolContext;
use crate::gating::Gate;
use crate::meta::{Tier, Toolset};
use crate::registry::Registry;
use crate::server::Backends;
use super::Report;
use crate::tools::common::pretty;
pub async fn policy(config: &Config, backends: Backends, action: &PolicyCommand) -> Report {
let path = match action {
PolicyCommand::Check { file } | PolicyCommand::Deploy { file } => file.as_path(),
};
let document = match std::fs::read_to_string(path) {
Ok(document) => document,
Err(error) => {
return Report {
text: format!("could not read {}: {error}\n", path.display()),
ok: false,
};
}
};
let context = match crate::server::build(config, crate::tools::entries(), backends).await {
Ok(startup) => Arc::clone(startup.server.context()),
Err(error) => {
return Report {
text: format!("{error}\n"),
ok: false,
};
}
};
match action {
PolicyCommand::Check { .. } => check_policy(&context, &document).await,
PolicyCommand::Deploy { .. } => deploy_policy(&context, &document, path).await,
}
}
async fn check_policy(
context: &std::sync::Arc<crate::context::ToolContext>,
document: &str,
) -> Report {
match call(
context,
"tailnet_policy_validate",
json!({"policy": document}),
)
.await
{
Ok(_) => Report::ok(""),
Err(refused) => Report {
text: refused.text,
ok: false,
},
}
}
async fn deploy_policy(context: &Arc<ToolContext>, document: &str, path: &Path) -> Report {
let current = match call(context, "tailnet_policy_get", json!({})).await {
Ok(current) => current,
Err(refused) => {
return Report {
text: refused.text,
ok: false,
};
}
};
let etag = current["etag"].as_str().map(str::to_owned);
let mut args = json!({"policy": document});
match &etag {
Some(etag) => args["etag"] = Value::String(etag.clone()),
None => args["over_default"] = Value::Bool(true),
}
match call(context, "tailnet_policy_set", args).await {
Ok(_) => Report::ok(""),
Err(Refused {
text,
conflict: false,
}) => Report { text, ok: false },
Err(Refused { text, .. }) => Report {
text: format!(
"{text}\nThe policy was read at {}, so somebody else wrote to this tailnet in \
between. Read it again, merge {} into what is there now, and deploy that.\n",
etag.as_deref().unwrap_or("no version"),
path.display()
),
ok: false,
},
}
}
struct Refused {
text: String,
conflict: bool,
}
async fn call(context: &Arc<ToolContext>, tool: &str, args: Value) -> Result<Value, Refused> {
let registry = match Registry::new(crate::tools::entries()) {
Ok(registry) => registry,
Err(error) => {
return Err(Refused {
text: format!("{error}\n"),
conflict: false,
});
}
};
let gate = Gate::unchecked(
BTreeSet::from([Toolset::TailnetPolicy]),
Tier::Destructive,
BTreeSet::new(),
);
let arguments = args.as_object().cloned().unwrap_or_default();
let refused = |error: crate::error::ToolError| Refused {
conflict: error.code == crate::error::ErrorCode::Conflict,
text: pretty(&error.to_value()) + "\n",
};
let (entry, arguments) = registry.resolve(tool, arguments, &gate).map_err(refused)?;
(entry.invoke)(context.clone(), arguments)
.await
.map_err(refused)
}