gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **The directory import, one per account at a time, on its own jera lane**
//! (A34-17).
//!
//! `Sso.ImportUsers` used to run up to 10 000 store writes inside the gRPC
//! call, on the async runtime, and a failure half way left half the file
//! applied. Now the call checks the caller and the size and enqueues; this
//! kind runs [`gunnar_grpc::sso::scim::run_import`] — edda's plan, applied in
//! ONE identity-table write or not at all — and the console polls
//! `Sso.ImportStatus`. Its own lane: an import waits behind nobody's gc or
//! purge, and nobody's gc waits behind it.

use gunnar_grpc::sso::scim::{ImportJob, ImportOrder, Imported};

use crate::job_queue::{Job, JobKind, JobQueue, JobState, Reporter};

/// What an import came to (the shared crate's counts and ended sessions).
#[derive(Debug, Clone, Default)]
pub struct ImportReport {
    pub outcome: Imported,
}

/// **The import kind.**
pub struct SsoImport;

impl JobKind for SsoImport {
    const NAME: &'static str = "sso-import";

    type Order = ImportOrder;
    type Report = ImportReport;

    /// The account: one import per account at a time.
    fn key(order: &ImportOrder) -> &str {
        &order.account
    }

    fn run(&self, order: &ImportOrder, report: &Reporter<SsoImport>) -> Result<(), String> {
        let progress = |done: u64, total: u64| report.progress(done, total);
        let done = gunnar_grpc::sso::scim::run_import(order, crate::job_queue::now_ms(), &progress);
        (order.finished)(&done);
        let done = done?;
        report.report(|r| r.outcome = done);
        Ok(())
    }
}

/// The queue the control plane holds.
pub type SsoImportQueue = JobQueue<SsoImport>;

/// A job as the seam answers it.
pub fn job_out(job: Job<SsoImport>) -> ImportJob {
    let (done_rows, total_rows) = job.progress.unwrap_or((0, 0));
    let mut outcome = job.report.outcome.clone();
    outcome.dry_run = job.order.dry_run;
    ImportJob {
        job_id: job.job_id.clone(),
        state: match job.state {
            JobState::Queued => "queued",
            JobState::Running => "running",
            JobState::Done => "done",
            JobState::Failed => "failed",
        },
        error: job.error.clone().unwrap_or_default(),
        done_rows,
        total_rows,
        outcome,
    }
}