Source commit: 09ca780bd67a00467e78139cf38466b8201b66ca Public tree identity: sha256:2f744c260b5fc2cf074ad9129f97f87f03714902e5b9fe174128afdc342ec2d0
236 lines
8.6 KiB
Rust
236 lines
8.6 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use anyhow::Result;
|
|
use clusterflux_core::admin_request_proof;
|
|
use serde_json::{json, Value};
|
|
|
|
use crate::client::JsonLineSession;
|
|
use crate::project::project_init_report;
|
|
use crate::tools::{command_nonce, unix_timestamp_seconds};
|
|
use crate::{
|
|
confirmation_required_report, AdminBootstrapArgs, AdminStatusArgs, AdminSuspendTenantArgs,
|
|
ProjectInitArgs,
|
|
};
|
|
|
|
pub(crate) fn admin_status_report(args: AdminStatusArgs) -> Result<Value> {
|
|
if let Some(coordinator) = &args.scope.coordinator {
|
|
let tenant = args.scope.tenant.clone();
|
|
let user = args.scope.user.clone();
|
|
let admin_token = admin_token_for_request(args.admin_token.as_deref())?;
|
|
let admin_nonce = command_nonce("admin-status");
|
|
let issued_at_epoch_seconds = unix_timestamp_seconds();
|
|
let admin_proof = admin_request_proof(
|
|
&admin_token,
|
|
"admin_status",
|
|
&tenant,
|
|
&user,
|
|
&tenant,
|
|
&admin_nonce,
|
|
issued_at_epoch_seconds,
|
|
);
|
|
let mut session = JsonLineSession::connect(coordinator)?;
|
|
let response = session.request(json!({
|
|
"type": "admin_status",
|
|
"tenant": tenant,
|
|
"actor_user": user,
|
|
"admin_proof": admin_proof,
|
|
"admin_nonce": admin_nonce,
|
|
"issued_at_epoch_seconds": issued_at_epoch_seconds,
|
|
}))?;
|
|
return Ok(json!({
|
|
"command": "admin status",
|
|
"coordinator": coordinator,
|
|
"tenant": tenant,
|
|
"user": user,
|
|
"suspended": response
|
|
.get("suspended")
|
|
.cloned()
|
|
.unwrap_or(json!(false)),
|
|
"response": response,
|
|
"safe_default": "read_only",
|
|
"private_website_required": false,
|
|
"coordinator_session_requests": session.requests(),
|
|
}));
|
|
}
|
|
Ok(json!({
|
|
"command": "admin status",
|
|
"mode": "self_hosted_local",
|
|
"safe_default": "read_only",
|
|
"private_website_required": false,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn admin_bootstrap_report(args: AdminBootstrapArgs, cwd: PathBuf) -> Result<Value> {
|
|
let scope = args.scope.clone();
|
|
let new_project = args.scope.project.clone();
|
|
let project_init = project_init_report(
|
|
ProjectInitArgs {
|
|
scope: args.scope,
|
|
new_project,
|
|
name: args.name,
|
|
yes: args.yes,
|
|
},
|
|
cwd,
|
|
)?;
|
|
let coordinator = scope
|
|
.coordinator
|
|
.clone()
|
|
.unwrap_or_else(|| "<local-coordinator>".to_owned());
|
|
let tenant = scope.tenant.clone();
|
|
let project = scope.project.clone();
|
|
let user = scope.user.clone();
|
|
Ok(json!({
|
|
"command": "admin bootstrap",
|
|
"mode": if scope.coordinator.is_some() { "public_coordinator_api" } else { "self_hosted_local" },
|
|
"tenant": tenant.clone(),
|
|
"project": project.clone(),
|
|
"user": user,
|
|
"coordinator": scope.coordinator,
|
|
"private_website_required": false,
|
|
"self_hosted_cli_only": true,
|
|
"project_config_written": project_init
|
|
.get("project_config_written")
|
|
.cloned()
|
|
.unwrap_or(json!(false)),
|
|
"project_init": project_init,
|
|
"admin_surfaces": {
|
|
"coordinator": "clusterflux-coordinator",
|
|
"project": "clusterflux project init/status/list/select",
|
|
"node": "clusterflux node enroll/list/status/revoke",
|
|
"process": "clusterflux run/process status/process restart/process cancel",
|
|
"logs": "clusterflux logs",
|
|
"artifacts": "clusterflux artifact list/download/export",
|
|
"quota": "clusterflux quota status",
|
|
"policy": "clusterflux admin status/suspend-tenant",
|
|
},
|
|
"bootstrap_sequence": [
|
|
{
|
|
"step": "start_self_hosted_coordinator",
|
|
"command": "clusterflux-coordinator --listen 127.0.0.1:0",
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "create_or_link_project",
|
|
"command": "clusterflux project init --yes",
|
|
"completed": true,
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "create_node_enrollment_grant",
|
|
"command": format!(
|
|
"clusterflux node enroll --coordinator {coordinator} --tenant {} --project-id {}",
|
|
tenant, project
|
|
),
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "attach_worker_node",
|
|
"command": format!(
|
|
"clusterflux node attach --coordinator {coordinator} --tenant {} --project-id {} --worker",
|
|
tenant, project
|
|
),
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "run_process",
|
|
"command": format!(
|
|
"clusterflux run --coordinator {coordinator} --tenant {} --project-id {}",
|
|
tenant, project
|
|
),
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "inspect_status_logs_artifacts",
|
|
"commands": [
|
|
"clusterflux process status",
|
|
"clusterflux task list",
|
|
"clusterflux logs",
|
|
"clusterflux artifact list",
|
|
"clusterflux quota status",
|
|
],
|
|
"private_website_required": false,
|
|
},
|
|
{
|
|
"step": "revoke_access",
|
|
"command": "clusterflux admin revoke-node --node <node-id> --yes",
|
|
"private_website_required": false,
|
|
}
|
|
],
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn admin_suspend_tenant_report(args: AdminSuspendTenantArgs) -> Result<Value> {
|
|
let tenant = args
|
|
.target_tenant
|
|
.unwrap_or_else(|| args.scope.tenant.clone());
|
|
if !args.yes {
|
|
return Ok(confirmation_required_report(
|
|
"admin suspend-tenant",
|
|
"suspend_tenant",
|
|
json!({
|
|
"coordinator": args.scope.coordinator,
|
|
"actor_tenant": args.scope.tenant,
|
|
"actor_user": args.scope.user,
|
|
"target_tenant": tenant,
|
|
}),
|
|
"clusterflux admin suspend-tenant --yes".to_owned(),
|
|
));
|
|
}
|
|
if let Some(coordinator) = &args.scope.coordinator {
|
|
let actor_tenant = args.scope.tenant.clone();
|
|
let actor_user = args.scope.user.clone();
|
|
let admin_token = admin_token_for_request(args.admin_token.as_deref())?;
|
|
let admin_nonce = command_nonce("admin-suspend-tenant");
|
|
let issued_at_epoch_seconds = unix_timestamp_seconds();
|
|
let admin_proof = admin_request_proof(
|
|
&admin_token,
|
|
"suspend_tenant",
|
|
&actor_tenant,
|
|
&actor_user,
|
|
&tenant,
|
|
&admin_nonce,
|
|
issued_at_epoch_seconds,
|
|
);
|
|
let mut session = JsonLineSession::connect(coordinator)?;
|
|
let response = session.request(json!({
|
|
"type": "suspend_tenant",
|
|
"tenant": actor_tenant,
|
|
"actor_user": actor_user,
|
|
"target_tenant": tenant,
|
|
"admin_proof": admin_proof,
|
|
"admin_nonce": admin_nonce,
|
|
"issued_at_epoch_seconds": issued_at_epoch_seconds,
|
|
}))?;
|
|
return Ok(json!({
|
|
"command": "admin suspend-tenant",
|
|
"coordinator": coordinator,
|
|
"requires_confirmation": !args.yes,
|
|
"tenant": tenant,
|
|
"actor_tenant": actor_tenant,
|
|
"actor_user": actor_user,
|
|
"suspended": response.get("type").and_then(Value::as_str) == Some("tenant_suspended"),
|
|
"private_website_required": false,
|
|
"response": response,
|
|
"coordinator_session_requests": session.requests(),
|
|
}));
|
|
}
|
|
Ok(json!({
|
|
"command": "admin suspend-tenant",
|
|
"status": "requires_coordinator",
|
|
"requires_confirmation": !args.yes,
|
|
"tenant": tenant,
|
|
"private_website_required": false,
|
|
}))
|
|
}
|
|
|
|
fn admin_token_for_request(explicit: Option<&str>) -> Result<String> {
|
|
explicit
|
|
.map(str::to_owned)
|
|
.or_else(|| std::env::var("CLUSTERFLUX_ADMIN_TOKEN").ok())
|
|
.filter(|token| !token.trim().is_empty())
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"admin command requires --admin-token or CLUSTERFLUX_ADMIN_TOKEN for coordinator requests"
|
|
)
|
|
})
|
|
}
|