Skip to main content

vcon_server/
commands.rs

1//! The commands other than `serve`: tenants, keys and the audit logs. Each
2//! takes the data directory as its work shares it, opens the registry with
3//! recovery when it runs alone and without when it may run beside a server,
4//! and prints what the matching admin route answers: one JSON object a
5//! line.
6
7use std::fmt;
8use std::io::{self, Write};
9use std::sync::Arc;
10
11use crate::access::{Scope, Scopes};
12use crate::audit::{Actor, AuditError};
13use crate::backend;
14use crate::clock::{Clock, SystemClock};
15use crate::config::{Config, StorageError};
16use crate::datadir::{self, LockError, Sharing, Work};
17use crate::envelope::{Key, KeyError};
18use crate::fs::OsFs;
19use crate::ids::{KeyId, TenantId};
20use crate::retention::Retention;
21use crate::store::ExamineError;
22use crate::tenants::{
23    self, BackupError, OpenFailure, RegistryError, Removal, TenantError, TenantExamination,
24    Tenants, Unexamined,
25};
26use time::OffsetDateTime;
27use time::format_description::well_known::Rfc3339;
28
29/// A command, as written on the command line.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum Command {
32    TenantCreate {
33        name: String,
34        retention: Option<String>,
35    },
36    TenantList,
37    /// `retention` is a period, or `default` for the server's.
38    TenantSetRetention {
39        id: String,
40        retention: String,
41    },
42    TenantRemove {
43        id: String,
44        force: bool,
45        override_holds: bool,
46    },
47    /// `metadata` is `none`, `counts` or `full`.
48    TenantSetMetadata {
49        id: String,
50        metadata: String,
51    },
52    /// Moves every tenant's sealed objects from the data directory to the
53    /// bucket `storage.objects = "s3"` names. Runs only while no server
54    /// does.
55    StorageMoveObjects,
56    /// Writes the tenant's kept versions as Parquet files to `to`, a
57    /// directory or an `s3://bucket/prefix`, beside a running server.
58    /// `since` and `until` are RFC 3339 times.
59    ExportParquet {
60        tenant: String,
61        to: String,
62        since: Option<String>,
63        until: Option<String>,
64    },
65    KeyCreate {
66        owner: Owner,
67        label: String,
68    },
69    KeyList {
70        tenant: Option<String>,
71    },
72    KeyRevoke {
73        id: String,
74    },
75    AuditExport {
76        log: Log,
77        since: Option<i64>,
78    },
79    /// Rotates a tenant's key.
80    RekeyTenant {
81        id: String,
82    },
83    /// Rotates the master key to the one in `new_master`.
84    RekeyMaster {
85        new_master: std::path::PathBuf,
86    },
87    /// Examines every tenant and reports what needs attention.
88    Check,
89    /// Writes a consistent copy of the data directory to `to`, which must
90    /// not exist, beside a running server.
91    Backup {
92        to: std::path::PathBuf,
93    },
94}
95
96/// Whose key `key create` makes.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum Owner {
99    Tenant { id: String, scopes: Vec<String> },
100    Operator,
101}
102
103/// Which audit log `audit export` writes.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Log {
106    Tenant(String),
107    Admin,
108}
109
110impl Command {
111    /// The work this command does in the data directory.
112    pub fn work(&self) -> Work {
113        match self {
114            Command::TenantCreate { .. } => Work::TenantCreate,
115            Command::TenantList => Work::TenantList,
116            Command::TenantSetRetention { .. } => Work::TenantSetRetention,
117            Command::TenantRemove { .. } => Work::TenantRemove,
118            Command::TenantSetMetadata { .. } => Work::TenantSetMetadata,
119            Command::ExportParquet { .. } => Work::ExportParquet,
120            Command::StorageMoveObjects => Work::StorageMoveObjects,
121            Command::KeyCreate { .. } => Work::KeyCreate,
122            Command::KeyList { .. } => Work::KeyList,
123            Command::KeyRevoke { .. } => Work::KeyRevoke,
124            Command::AuditExport { .. } => Work::AuditExport,
125            Command::RekeyTenant { .. } => Work::RekeyTenant,
126            Command::RekeyMaster { .. } => Work::RekeyMaster,
127            Command::Check => Work::Check,
128            Command::Backup { .. } => Work::Backup,
129        }
130    }
131}
132
133/// Why a command did not complete.
134#[derive(Debug)]
135pub enum CommandError {
136    DataDir(LockError),
137    MasterKey(KeyError),
138    /// server.sqlite was made under another master key.
139    WrongMasterKey,
140    /// A period that is neither like 7y, 18m or 90d nor, where allowed,
141    /// `default`.
142    BadPeriod(String),
143    Tenant(TenantError),
144    /// The registry could not be opened.
145    Open(RegistryError),
146    /// No key has this id.
147    UnknownKey,
148    /// The output could not be written.
149    Output(io::Error),
150    /// The new master key is the one already in use.
151    SameMasterKey,
152    /// `check` found this many problems; its report says which.
153    CheckFound(usize),
154    Backup(BackupError),
155    /// The storage the configuration names cannot be used.
156    Storage(StorageError),
157    /// Not `none`, `counts` or `full`.
158    BadLevel(String),
159    /// Not an RFC 3339 time.
160    BadTime(String),
161    /// Neither a directory nor an `s3://bucket/prefix`.
162    BadTarget(String),
163    /// An `s3://` target with no `[export.s3]` or `[storage.s3]` table, or
164    /// in a build without S3.
165    NoS3,
166    Export(crate::export::ExportError),
167    /// Moving a tenant's objects to the bucket stopped; what was moved
168    /// stays moved, and running the command again moves the rest.
169    Move {
170        tenant: TenantId,
171        why: String,
172    },
173}
174
175impl fmt::Display for CommandError {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            CommandError::DataDir(e) => write!(f, "{e}"),
179            CommandError::MasterKey(e) => write!(f, "the master key cannot be read: {e:?}"),
180            CommandError::Open(e) => write!(
181                f,
182                "the registry cannot be opened: {}",
183                crate::tenants::with_causes(e)
184            ),
185            CommandError::WrongMasterKey | CommandError::Tenant(TenantError::WrongKey) => {
186                f.write_str("the master key is not the one server.sqlite was made under")
187            }
188            CommandError::BadPeriod(text) => write!(
189                f,
190                "{text:?} is not a period such as 7y, 18m or 90d (or, for set-retention, default)"
191            ),
192            CommandError::UnknownKey => f.write_str("no key has this id"),
193            CommandError::Output(e) => write!(f, "the output could not be written: {e}"),
194            CommandError::SameMasterKey => {
195                f.write_str("the new master key is the one already in use")
196            }
197            CommandError::CheckFound(n) => write!(f, "check found {n} problem(s); see its report"),
198            CommandError::Storage(e) => write!(f, "the storage cannot be used: {e}"),
199            CommandError::BadLevel(text) => write!(
200                f,
201                "{text:?} is not a metadata level: none, counts or full"
202            ),
203            CommandError::BadTime(text) => write!(
204                f,
205                "{text:?} is not an RFC 3339 time such as 2026-11-05T00:00:00Z"
206            ),
207            CommandError::BadTarget(text) => write!(
208                f,
209                "{text:?} is not a directory or an s3://bucket/prefix"
210            ),
211            CommandError::NoS3 => f.write_str(
212                "an s3:// target needs an [export.s3] or [storage.s3] table, in a build with the s3 feature",
213            ),
214            CommandError::Export(e) => write!(f, "{e}"),
215            CommandError::Move { tenant, why } => write!(
216                f,
217                "moving tenant {tenant}'s objects to the bucket stopped: {why}; what moved stays \
218                 moved, and running storage move-objects again moves the rest"
219            ),
220            CommandError::Backup(e) => f.write_str(match e {
221                BackupError::Exists => {
222                    "the backup's path already exists; a backup is written to a new directory"
223                }
224                BackupError::KeptChanging => {
225                    "tenants were removed or versions purged during each try; try again"
226                }
227                BackupError::Io => "the backup could not be written, or the data could not be read",
228                BackupError::AuditFailed => "the audit record could not be written",
229            }),
230            CommandError::Tenant(e) => f.write_str(match e {
231                TenantError::NotFound => "no tenant has this id",
232                TenantError::NameTaken => "another tenant has this name",
233                TenantError::BadName => {
234                    "a tenant name is 1 to 128 characters, with no control characters and no space at either end"
235                }
236                TenantError::HoldsVcons => "the tenant holds vCons; add --force to remove them too",
237                TenantError::UnderHold => {
238                    "a vCon of the tenant is under a legal hold; only --override-holds removes it"
239                }
240                TenantError::BadScopes => {
241                    "a tenant key's scopes are one or more of write, read, delete, legal and audit, each named once"
242                }
243                TenantError::LimitReached => {
244                    "the master key has wrapped all the tenant keys it may; run rekey master"
245                }
246                TenantError::AuditFailed => "the audit record could not be written",
247                TenantError::Closing => "the tenant is being removed",
248                TenantError::CliOnly => "only the command line may do this",
249                TenantError::WrongKey => unreachable!("matched above"),
250                TenantError::Io => "the data could not be read or written",
251            }),
252        }
253    }
254}
255
256/// Runs `command` against the data directory `config` names, writing its
257/// answer to `out`.
258pub fn run(config: &Config, command: &Command, out: &mut dyn Write) -> Result<(), CommandError> {
259    let sharing = command.work().sharing();
260    let _lock = datadir::lock(&config.data_dir, sharing).map_err(CommandError::DataDir)?;
261    let master = Key::load_master(&config.master_key).map_err(CommandError::MasterKey)?;
262    // A master rotation renames over server.sqlite, so nothing may hold it
263    // open: it runs before the registry is opened.
264    if let Command::RekeyMaster { new_master } = command {
265        return rekey_master(config, &master, new_master, out);
266    }
267    // A command beside a running server must not run start-up recovery: it
268    // would take the server's work in progress for a crash's leftovers. Nor
269    // does `check`, which reports what recovery would remove.
270    let open = if sharing == Sharing::Beside || matches!(command, Command::Check) {
271        Tenants::open_beside_in
272    } else {
273        Tenants::open_in
274    };
275    let objects =
276        backend::objects_for(&config.storage, &config.data_dir).map_err(CommandError::Storage)?;
277    // The bucket the objects move to, for `storage move-objects`.
278    #[cfg(feature = "s3")]
279    let bucket = match &objects {
280        backend::ObjectPlace::S3(s3) => Some(s3.clone()),
281        backend::ObjectPlace::Local => None,
282    };
283    let tenants = open(
284        &config.data_dir,
285        master,
286        Arc::new(OsFs),
287        Arc::new(SystemClock),
288        objects,
289    )
290    .map_err(|e| match e {
291        RegistryError::WrongKey => CommandError::WrongMasterKey,
292        other => CommandError::Open(other),
293    })?;
294    let who = Actor::Cli;
295    match command {
296        Command::TenantCreate { name, retention } => {
297            let retention = retention.as_deref().map(period).transpose()?;
298            let id = tenants.create(who, name, retention)?;
299            line(out, &serde_json::json!({ "id": id.to_string() }))
300        }
301        Command::TenantList => {
302            for tenant in tenants.list(who)? {
303                line(out, &tenant.to_json())?;
304            }
305            Ok(())
306        }
307        Command::TenantSetRetention { id, retention } => {
308            let retention = match retention.as_str() {
309                "default" => None,
310                text => Some(period(text)?),
311            };
312            let id = tenant_id(id)?;
313            tenants.set_retention(who, id, retention)?;
314            line(
315                out,
316                &serde_json::json!({
317                    "id": id.to_string(),
318                    "retention": retention.map(|r| r.to_string()),
319                }),
320            )
321        }
322        Command::TenantRemove {
323            id,
324            force,
325            override_holds,
326        } => {
327            let removal = match (force, override_holds) {
328                (_, true) => Removal::ForceOverridingHolds,
329                (true, false) => Removal::Force,
330                (false, false) => Removal::Empty,
331            };
332            Ok(tenants.remove(who, tenant_id(id)?, removal)?)
333        }
334        Command::KeyCreate { owner, label } => {
335            let (tenant, scopes) = match owner {
336                Owner::Tenant { id, scopes } => (
337                    Some(tenant_id(id)?),
338                    Scopes::named(scopes).ok_or(CommandError::Tenant(TenantError::BadScopes))?,
339                ),
340                Owner::Operator => (None, Scopes::of(&[Scope::Admin])),
341            };
342            let made = tenants.create_key(who, tenant, scopes, label)?;
343            line(
344                out,
345                &serde_json::json!({ "id": made.id.to_string(), "token": made.token }),
346            )
347        }
348        Command::KeyList { tenant } => {
349            let tenant = tenant.as_deref().map(tenant_id).transpose()?;
350            for key in tenants.list_keys(who, tenant)? {
351                let shown = key.to_json().ok_or(CommandError::Tenant(TenantError::Io))?;
352                line(out, &shown)?;
353            }
354            Ok(())
355        }
356        Command::KeyRevoke { id } => {
357            let id = KeyId::parse(id).ok_or(CommandError::UnknownKey)?;
358            tenants.revoke_key(who, id).map_err(|e| match e {
359                TenantError::NotFound => CommandError::UnknownKey,
360                other => CommandError::Tenant(other),
361            })
362        }
363        Command::TenantSetMetadata { id, metadata } => {
364            let level = crate::export::Metadata::parse(metadata)
365                .ok_or_else(|| CommandError::BadLevel(metadata.clone()))?;
366            let id = tenant_id(id)?;
367            tenants.set_metadata(who, id, level)?;
368            line(
369                out,
370                &serde_json::json!({ "id": id.to_string(), "metadata": level.as_str() }),
371            )
372        }
373        Command::ExportParquet {
374            tenant,
375            to,
376            since,
377            until,
378        } => export_parquet(
379            config,
380            &tenants,
381            tenant,
382            to,
383            since.as_deref(),
384            until.as_deref(),
385            out,
386        ),
387        Command::RekeyTenant { id } => Ok(tenants.rekey_tenant(who, tenant_id(id)?)?),
388        Command::RekeyMaster { .. } => unreachable!("rotated before the registry is opened"),
389        Command::Check => check(config, &tenants, out),
390        #[cfg(feature = "s3")]
391        Command::StorageMoveObjects => match bucket {
392            Some(s3) => move_objects(config, &tenants, &s3, out),
393            None => Err(not_a_bucket()),
394        },
395        #[cfg(not(feature = "s3"))]
396        Command::StorageMoveObjects => Err(not_a_bucket()),
397        Command::Backup { to } => {
398            let done = tenants.backup(who, to).map_err(CommandError::Backup)?;
399            line(
400                out,
401                &serde_json::json!({
402                    "tenants": done.tenants,
403                    "versions": done.versions,
404                    "tries": done.tries,
405                }),
406            )
407        }
408        Command::AuditExport { log, since } => {
409            let exported = match log {
410                Log::Admin => tenants.export_admin(*since, out),
411                Log::Tenant(id) => tenants.store(tenant_id(id)?)?.export(*since, out),
412            };
413            exported.map_err(|e| match e {
414                AuditError::Write(e) => CommandError::Output(e),
415                AuditError::Sqlite(_) => CommandError::Tenant(TenantError::Io),
416            })
417        }
418    }
419}
420
421impl From<TenantError> for CommandError {
422    fn from(e: TenantError) -> CommandError {
423        CommandError::Tenant(e)
424    }
425}
426
427/// Exports a tenant's kept versions as Parquet files and prints what was
428/// written. Every argument is checked before anything is written.
429fn export_parquet(
430    config: &Config,
431    tenants: &Tenants,
432    tenant: &str,
433    to: &str,
434    since: Option<&str>,
435    until: Option<&str>,
436    out: &mut dyn Write,
437) -> Result<(), CommandError> {
438    use crate::export::target::Place;
439    use crate::export::{self, Plan};
440    let time = |text: Option<&str>| {
441        text.map(|t| {
442            OffsetDateTime::parse(t, &Rfc3339)
443                .map(crate::clock::nanos)
444                .map_err(|_| CommandError::BadTime(t.to_owned()))
445        })
446        .transpose()
447    };
448    let (since, until) = (time(since)?, time(until)?);
449    let place = Place::parse(to).ok_or_else(|| CommandError::BadTarget(to.to_owned()))?;
450    let id = tenant_id(tenant)?;
451    let level = tenants.metadata(id)?;
452    let target: Box<dyn export::Target> = match place {
453        Place::Dir(dir) => Box::new(export::DirTarget::new(&dir, Arc::new(OsFs))),
454        Place::S3 { bucket, prefix } => s3_target(config, &bucket, &prefix)?,
455    };
456    let store = tenants.store(id)?;
457    let plan = Plan {
458        level,
459        since,
460        until,
461        sizes: export::SIZES,
462        limits: vcon::read::Limits::new(config.inflated_limit),
463    };
464    let done = export::export(&store, &plan, target.as_ref(), SystemClock.now())
465        .map_err(CommandError::Export)?;
466    line(
467        out,
468        &serde_json::json!({
469            "tenant": id.to_string(),
470            "metadata": level.as_str(),
471            "to": target.describe(),
472            "manifest": done.manifest,
473            "seq": done.seq,
474            "files": done.files.len(),
475            "rows": done.rows,
476            "bytes": done.bytes,
477            "skipped": done.skipped,
478            "unread": done.unread,
479        }),
480    )
481}
482
483/// The bucket target `--to` names, reached through `[export.s3]`, else
484/// `[storage.s3]`'s service and keys.
485#[cfg(feature = "s3")]
486fn s3_target(
487    config: &Config,
488    bucket: &str,
489    prefix: &str,
490) -> Result<Box<dyn crate::export::Target>, CommandError> {
491    use crate::config::{ExportS3, ObjectStore};
492    let (service, table) = match (&config.export.s3, &config.storage.objects) {
493        (Some(service), _) => (service.clone(), "export.s3"),
494        (None, ObjectStore::S3(storage)) => (ExportS3::from(storage), "storage.s3"),
495        (None, ObjectStore::Local) => return Err(CommandError::NoS3),
496    };
497    let target = crate::export::s3::S3Target::connect(&service.for_bucket(bucket, prefix), table)
498        .map_err(CommandError::Storage)?;
499    Ok(Box::new(target))
500}
501
502#[cfg(not(feature = "s3"))]
503fn s3_target(
504    _config: &Config,
505    _bucket: &str,
506    _prefix: &str,
507) -> Result<Box<dyn crate::export::Target>, CommandError> {
508    Err(CommandError::NoS3)
509}
510
511/// Rotates the master key and says what the operator does next.
512fn rekey_master(
513    config: &Config,
514    master: &Key,
515    new_master: &std::path::Path,
516    out: &mut dyn Write,
517) -> Result<(), CommandError> {
518    let new = Key::load_master(new_master).map_err(CommandError::MasterKey)?;
519    if new.id() == master.id() {
520        return Err(CommandError::SameMasterKey);
521    }
522    tenants::rekey_master(&config.data_dir, master, &new, SystemClock.now()).map_err(
523        |e| match e {
524            TenantError::WrongKey => CommandError::WrongMasterKey,
525            other => CommandError::Tenant(other),
526        },
527    )?;
528    line(
529        out,
530        &serde_json::json!({
531            "master_key_id": new.id().to_string(),
532            "next": "point master_key in the configuration at the new key file, start the server, then destroy every copy of the old master key",
533        }),
534    )
535}
536
537/// Examines every tenant, prints a line for each and a summary, and fails
538/// when anything needs the operator.
539/// The refusal of `storage move-objects` with the objects kept in the data
540/// directory.
541fn not_a_bucket() -> CommandError {
542    CommandError::Storage(StorageError(
543        "storage move-objects moves the objects to the bucket that storage.objects = \"s3\" \
544         names; this configuration keeps them in the data directory"
545            .to_owned(),
546    ))
547}
548
549/// `storage move-objects`: each tenant's object files, under its directory
550/// in the data directory, moved to the bucket, as `backend::migrate` does;
551/// one line per tenant with what moved and what `check` then finds missing,
552/// and a total.
553#[cfg(feature = "s3")]
554fn move_objects(
555    config: &Config,
556    tenants: &Tenants,
557    s3: &Arc<crate::backend::s3::S3>,
558    out: &mut dyn Write,
559) -> Result<(), CommandError> {
560    use crate::backend::migrate::MoveError;
561    let mut total = 0;
562    for id in tenants.ids()? {
563        let dir = config
564            .data_dir
565            .join("tenants")
566            .join(id.to_string())
567            .join("objects");
568        let moved = if dir.exists() {
569            let from = crate::backend::local::LocalObjects::new(dir, Arc::new(OsFs));
570            crate::backend::migrate::move_objects(&from, &s3.tenant(id)).map_err(|e| {
571                let why = match e {
572                    MoveError::From(e) => {
573                        format!("an object file could not be read or removed: {e}")
574                    }
575                    MoveError::To(e) => format!("the bucket could not be written or read: {e}"),
576                    MoveError::Differs(version) => format!(
577                        "version {version} read back from the bucket differs from its file, \
578                         which stays in the data directory"
579                    ),
580                };
581                CommandError::Move { tenant: id, why }
582            })?
583        } else {
584            0
585        };
586        let missing = tenants
587            .store(id)?
588            .check()
589            .map_err(|e| CommandError::Move {
590                tenant: id,
591                why: format!("the objects in the bucket could not be checked: {e:?}"),
592            })?
593            .len();
594        total += moved;
595        line(
596            out,
597            &serde_json::json!({ "tenant": id.to_string(), "moved": moved, "missing": missing }),
598        )?;
599    }
600    line(out, &serde_json::json!({ "moved": total }))
601}
602
603fn check(config: &Config, tenants: &Tenants, out: &mut dyn Write) -> Result<(), CommandError> {
604    let now = SystemClock.now();
605    let found = tenants.examine(config.retention_default, now)?;
606    let time = |at: Option<OffsetDateTime>| at.and_then(|at| at.format(&Rfc3339).ok());
607    let mut total = 0;
608    for tenant in &found.tenants {
609        let named = problems(
610            tenant,
611            config.purge_interval,
612            now,
613            config.storage.parquet.is_some(),
614        );
615        total += named.len();
616        let store = tenant.store.as_ref().ok();
617        line(
618            out,
619            &serde_json::json!({
620                "tenant": tenant.id.to_string(),
621                "name": tenant.name,
622                "versions": store.map(|s| s.versions),
623                "packed": store.map(|s| s.packed),
624                "damaged": store.map(|s| s.damaged.iter().map(ToString::to_string).collect::<Vec<_>>()),
625                "leftovers": store.map(|s| &s.leftovers),
626                "past_due": store.map(|s| s.due),
627                "oldest_past_due": time(store.and_then(|s| s.oldest_due_since)),
628                "purges_awaiting_rotation": store.map(|s| s.purges_since_key),
629                "oldest_purge_awaiting_rotation": time(store.and_then(|s| s.oldest_purge_since_key)),
630                "rotation_stopped": tenant.rotation_stopped,
631                "problems": named,
632            }),
633        )?;
634    }
635    line(
636        out,
637        &serde_json::json!({
638            "tenants": found.tenants.len(),
639            "removed_tenants_awaiting_master_rotation": found.removed_since_master,
640            "oldest_removal_awaiting_master_rotation": time(found.oldest_removed_since_master),
641            "problems": total,
642        }),
643    )?;
644    match total {
645        0 => Ok(()),
646        n => Err(CommandError::CheckFound(n)),
647    }
648}
649
650/// What in one tenant's examination needs the operator. A stalled sweep is
651/// one whose oldest due version has waited longer than `purge_interval`;
652/// a shorter wait is the next sweep's work. Packed versions are a problem
653/// when the configuration has no `[storage.parquet]` (`parquet` false),
654/// since `serve` then refuses to start. Leftover files and work waiting
655/// for rotation are reported but are not problems.
656pub fn problems(
657    tenant: &TenantExamination,
658    purge_interval: std::time::Duration,
659    now: OffsetDateTime,
660    parquet: bool,
661) -> Vec<String> {
662    let mut found = Vec::new();
663    if tenant.rotation_stopped {
664        found.push("a key rotation was stopped part way; run rekey tenant again".to_owned());
665    }
666    match &tenant.store {
667        Err(Unexamined::Open(OpenFailure::WrongKey)) => {
668            found.push("its catalog does not open under its tenant key".to_owned());
669        }
670        Err(Unexamined::Open(OpenFailure::Access { path, error })) => {
671            found.push(format!(
672                "its catalog cannot be opened: {}: {error}",
673                path.display()
674            ));
675        }
676        Err(Unexamined::Open(OpenFailure::Refused(e))) => {
677            found.push(format!("its catalog cannot be opened: {e:?}"));
678        }
679        Err(Unexamined::Examine(ExamineError::Objects { kind, os_error })) => {
680            let number = os_error
681                .map(|n| format!(" (os error {n})"))
682                .unwrap_or_default();
683            found.push(format!("its objects cannot be read: {kind}{number}"));
684        }
685        Err(Unexamined::Examine(ExamineError::Failed(e))) => {
686            found.push(format!("it could not be examined: {e:?}"));
687        }
688        Ok(store) => {
689            if store.packed > 0 && !parquet {
690                found.push(format!(
691                    "{} version(s) are packed into Parquet files, and the configuration has no \
692                     [storage.parquet]: serve refuses to start until the table is put back",
693                    store.packed
694                ));
695            }
696            for version in &store.damaged {
697                found.push(format!("version {version} is missing or does not open"));
698            }
699            if let Some(since) = store.oldest_due_since
700                && now - since > purge_interval
701            {
702                found.push(format!(
703                    "{} version(s) due for purge, the oldest longer than purge_interval: the sweep is not keeping up",
704                    store.due
705                ));
706            }
707        }
708    }
709    found
710}
711
712/// A period as a command line writes it.
713fn period(text: &str) -> Result<Retention, CommandError> {
714    Retention::parse(text).ok_or_else(|| CommandError::BadPeriod(text.to_owned()))
715}
716
717/// A tenant id as a command line writes it; one that does not parse names
718/// no tenant.
719fn tenant_id(text: &str) -> Result<TenantId, CommandError> {
720    TenantId::parse(text).ok_or(CommandError::Tenant(TenantError::NotFound))
721}
722
723/// Writes one JSON object as a line.
724fn line(out: &mut dyn Write, value: &serde_json::Value) -> Result<(), CommandError> {
725    writeln!(out, "{value}").map_err(CommandError::Output)
726}
727
728#[cfg(test)]
729mod tests {
730    use serde_json::{Value, json};
731
732    use super::*;
733    use crate::store::Caller;
734    use crate::store::testing::vcon;
735
736    const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
737
738    struct Setup {
739        _dir: tempfile::TempDir,
740        config: Config,
741    }
742
743    fn setup() -> Setup {
744        let dir = tempfile::tempdir().expect("temp dir");
745        let data = dir.path().join("data");
746        std::fs::create_dir(&data).expect("data dir");
747        let master = dir.path().join("master.key");
748        std::fs::write(&master, [0x5a; 32]).expect("master key file");
749        let config = crate::config::parse(&format!(
750            "listen = \"127.0.0.1:1\"\ndata_dir = {data:?}\nmaster_key = {master:?}\n\
751             [tls]\ncert = \"c\"\nkey = \"k\"\n"
752        ))
753        .expect("a configuration");
754        Setup { _dir: dir, config }
755    }
756
757    impl Setup {
758        /// Runs `command` and parses each line it prints.
759        fn run(&self, command: Command) -> Result<Vec<Value>, CommandError> {
760            let mut out = Vec::new();
761            run(&self.config, &command, &mut out)?;
762            Ok(String::from_utf8(out)
763                .expect("text")
764                .lines()
765                .map(|line| serde_json::from_str(line).expect("a JSON line"))
766                .collect())
767        }
768
769        fn create(&self, name: &str) -> String {
770            let out = self
771                .run(Command::TenantCreate {
772                    name: name.to_owned(),
773                    retention: None,
774                })
775                .expect("made");
776            out[0]["id"].as_str().expect("an id").to_owned()
777        }
778
779        /// The registry as a test reads it, with the command's locks free.
780        fn tenants(&self) -> Tenants {
781            Tenants::open_beside(
782                &self.config.data_dir,
783                Key::load_master(&self.config.master_key).expect("master"),
784                Arc::new(OsFs),
785                Arc::new(SystemClock),
786            )
787            .expect("opens")
788        }
789
790        fn tenant_dirs(&self) -> Vec<String> {
791            let mut names: Vec<String> = std::fs::read_dir(self.config.data_dir.join("tenants"))
792                .expect("tenants dir")
793                .map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
794                .collect();
795            names.sort();
796            names
797        }
798    }
799
800    fn tenant_error(result: Result<Vec<Value>, CommandError>) -> TenantError {
801        match result {
802            Err(CommandError::Tenant(e)) => e,
803            other => panic!("not a tenant error: {other:?}"),
804        }
805    }
806
807    #[test]
808    fn tenant_create_prints_the_new_id_and_list_shows_it() {
809        let s = setup();
810        let out = s
811            .run(Command::TenantCreate {
812                name: "acme".to_owned(),
813                retention: Some("18m".to_owned()),
814            })
815            .expect("made");
816        assert_eq!(out.len(), 1);
817        let id = out[0]["id"].as_str().expect("an id").to_owned();
818        assert!(TenantId::parse(&id).is_some(), "{id}");
819        assert_eq!(out[0], json!({ "id": id }));
820        let beta = s.create("beta");
821        assert_eq!(
822            s.run(Command::TenantList).expect("listed"),
823            [
824                json!({ "id": id, "name": "acme", "retention": "18m", "metadata": "none" }),
825                json!({ "id": beta, "name": "beta", "retention": null, "metadata": "none" }),
826            ]
827        );
828    }
829
830    #[test]
831    fn a_bad_period_is_refused_before_anything_is_made() {
832        let s = setup();
833        let error = s
834            .run(Command::TenantCreate {
835                name: "acme".to_owned(),
836                retention: Some("7w".to_owned()),
837            })
838            .expect_err("refused");
839        assert!(
840            matches!(&error, CommandError::BadPeriod(p) if p == "7w"),
841            "{error:?}"
842        );
843        assert!(error.to_string().contains("7w"), "{error}");
844        assert_eq!(
845            s.run(Command::TenantList).expect("listed"),
846            Vec::<Value>::new()
847        );
848        let id = s.create("acme");
849        let error = s
850            .run(Command::TenantSetRetention {
851                id,
852                retention: "forever".to_owned(),
853            })
854            .expect_err("refused");
855        assert!(matches!(&error, CommandError::BadPeriod(p) if p == "forever"));
856    }
857
858    #[test]
859    fn set_retention_takes_a_period_or_default() {
860        let s = setup();
861        let id = s.create("acme");
862        let set = |retention: &str| {
863            s.run(Command::TenantSetRetention {
864                id: id.clone(),
865                retention: retention.to_owned(),
866            })
867            .expect("set")
868        };
869        assert_eq!(set("90d"), [json!({ "id": id, "retention": "90d" })]);
870        assert_eq!(
871            s.run(Command::TenantList).expect("listed")[0]["retention"],
872            "90d"
873        );
874        assert_eq!(set("default"), [json!({ "id": id, "retention": null })]);
875        assert_eq!(
876            s.run(Command::TenantList).expect("listed")[0]["retention"],
877            Value::Null
878        );
879    }
880
881    #[test]
882    fn an_unknown_or_malformed_tenant_is_not_found() {
883        let s = setup();
884        for id in [TenantId::generate().to_string(), "not-an-id".to_owned()] {
885            assert_eq!(
886                tenant_error(s.run(Command::TenantSetRetention {
887                    id: id.clone(),
888                    retention: "7y".to_owned()
889                })),
890                TenantError::NotFound
891            );
892            assert_eq!(
893                tenant_error(s.run(Command::TenantRemove {
894                    id: id.clone(),
895                    force: true,
896                    override_holds: false
897                })),
898                TenantError::NotFound
899            );
900            assert_eq!(
901                tenant_error(s.run(Command::KeyCreate {
902                    owner: Owner::Tenant {
903                        id: id.clone(),
904                        scopes: vec!["read".to_owned()]
905                    },
906                    label: "l".to_owned()
907                })),
908                TenantError::NotFound
909            );
910            assert_eq!(
911                tenant_error(s.run(Command::KeyList {
912                    tenant: Some(id.clone())
913                })),
914                TenantError::NotFound
915            );
916            assert_eq!(
917                tenant_error(s.run(Command::AuditExport {
918                    log: Log::Tenant(id.clone()),
919                    since: None
920                })),
921                TenantError::NotFound
922            );
923        }
924    }
925
926    /// Without --force a tenant holding vCons stays; --force does not pass
927    /// a legal hold; --override-holds does.
928    #[test]
929    fn remove_goes_as_far_as_its_flags_allow() {
930        let s = setup();
931        let empty = s.create("empty");
932        let full = s.create("full");
933        {
934            let tenants = s.tenants();
935            let store = tenants
936                .store(TenantId::parse(&full).expect("id"))
937                .expect("store");
938            let caller = Caller { key: KeyId([1; 8]) };
939            let (found, body) = vcon(UUID, "kept");
940            store.create(&caller, &found, &body).expect("stored");
941            store.set_hold(&caller, found.uuid, true).expect("held");
942        }
943        let remove = |id: &str, force, override_holds| {
944            s.run(Command::TenantRemove {
945                id: id.to_owned(),
946                force,
947                override_holds,
948            })
949        };
950        assert_eq!(
951            remove(&empty, false, false).expect("removed"),
952            Vec::<Value>::new()
953        );
954        assert_eq!(
955            tenant_error(remove(&full, false, false)),
956            TenantError::HoldsVcons
957        );
958        assert_eq!(
959            tenant_error(remove(&full, true, false)),
960            TenantError::UnderHold
961        );
962        assert_eq!(
963            remove(&full, true, true).expect("removed"),
964            Vec::<Value>::new()
965        );
966        assert_eq!(
967            s.run(Command::TenantList).expect("listed"),
968            Vec::<Value>::new()
969        );
970        assert_eq!(s.tenant_dirs(), Vec::<String>::new());
971    }
972
973    #[test]
974    fn keys_are_made_listed_and_revoked() {
975        let s = setup();
976        let tenant = s.create("acme");
977        let made = s
978            .run(Command::KeyCreate {
979                owner: Owner::Tenant {
980                    id: tenant.clone(),
981                    scopes: vec!["write".to_owned(), "read".to_owned()],
982                },
983                label: "producer".to_owned(),
984            })
985            .expect("made");
986        let id = made[0]["id"].as_str().expect("id").to_owned();
987        let token = made[0]["token"].as_str().expect("token").to_owned();
988        assert_eq!(made.len(), 1);
989        assert_eq!(made[0].as_object().expect("object").len(), 2);
990        assert!(token.starts_with(&format!("vcs_{id}_")), "{token}");
991        let operator = s
992            .run(Command::KeyCreate {
993                owner: Owner::Operator,
994                label: "ops".to_owned(),
995            })
996            .expect("made")[0]["id"]
997            .as_str()
998            .expect("id")
999            .to_owned();
1000
1001        let listed = s.run(Command::KeyList { tenant: None }).expect("listed");
1002        assert_eq!(listed.len(), 2);
1003        assert_eq!(listed[0]["id"], id.as_str());
1004        assert_eq!(listed[0]["tenant"], tenant.as_str());
1005        assert_eq!(listed[0]["scopes"], json!(["write", "read"]));
1006        assert_eq!(listed[0]["label"], "producer");
1007        assert_eq!(listed[0]["revoked_at"], Value::Null);
1008        assert!(
1009            listed[0]["created_at"]
1010                .as_str()
1011                .is_some_and(|t| t.ends_with('Z'))
1012        );
1013        assert!(
1014            listed.iter().all(|k| k.get("token").is_none()),
1015            "never a token"
1016        );
1017        assert_eq!(listed[1]["id"], operator.as_str());
1018        assert_eq!(listed[1]["tenant"], Value::Null);
1019        assert_eq!(listed[1]["scopes"], json!(["admin"]));
1020
1021        let own = s
1022            .run(Command::KeyList {
1023                tenant: Some(tenant.clone()),
1024            })
1025            .expect("listed");
1026        assert_eq!(own, listed[..1]);
1027
1028        assert_eq!(
1029            s.run(Command::KeyRevoke { id: id.clone() })
1030                .expect("revoked"),
1031            Vec::<Value>::new()
1032        );
1033        let listed = s.run(Command::KeyList { tenant: None }).expect("listed");
1034        assert!(listed[0]["revoked_at"].as_str().is_some(), "{listed:?}");
1035        assert_eq!(listed[1]["revoked_at"], Value::Null);
1036    }
1037
1038    #[test]
1039    fn bad_scopes_and_unknown_keys_are_refused() {
1040        let s = setup();
1041        let tenant = s.create("acme");
1042        for scopes in [
1043            vec!["superuser"],
1044            vec!["read", "superuser"],
1045            vec!["read", "read"],
1046            vec!["admin"],
1047            vec![],
1048        ] {
1049            let error = s.run(Command::KeyCreate {
1050                owner: Owner::Tenant {
1051                    id: tenant.clone(),
1052                    scopes: scopes.iter().map(|s| (*s).to_owned()).collect(),
1053                },
1054                label: "l".to_owned(),
1055            });
1056            assert_eq!(tenant_error(error), TenantError::BadScopes, "{scopes:?}");
1057        }
1058        assert_eq!(
1059            s.run(Command::KeyList { tenant: None }).expect("listed"),
1060            Vec::<Value>::new()
1061        );
1062        for id in ["0123456789abcdef".to_owned(), "nope".to_owned()] {
1063            assert!(
1064                matches!(
1065                    s.run(Command::KeyRevoke { id }),
1066                    Err(CommandError::UnknownKey)
1067                ),
1068                "unknown"
1069            );
1070        }
1071    }
1072
1073    #[test]
1074    fn audit_export_writes_either_log_from_a_sequence_number() {
1075        let s = setup();
1076        let tenant = s.create("acme");
1077        s.run(Command::TenantList).expect("listed");
1078        s.run(Command::KeyCreate {
1079            owner: Owner::Tenant {
1080                id: tenant.clone(),
1081                scopes: vec!["read".to_owned()],
1082            },
1083            label: "l".to_owned(),
1084        })
1085        .expect("made");
1086
1087        let admin = s
1088            .run(Command::AuditExport {
1089                log: Log::Admin,
1090                since: None,
1091            })
1092            .expect("exported");
1093        let actions: Vec<&str> = admin
1094            .iter()
1095            .map(|r| r["action"].as_str().expect("action"))
1096            .collect();
1097        assert_eq!(actions, ["tenant_create", "tenant_list", "key_create"]);
1098        assert!(admin.iter().all(|r| r["actor"] == "cli"));
1099        let since = admin[0]["seq"].as_i64().expect("seq");
1100        let later = s
1101            .run(Command::AuditExport {
1102                log: Log::Admin,
1103                since: Some(since),
1104            })
1105            .expect("exported");
1106        assert_eq!(later, admin[1..]);
1107
1108        let own = s
1109            .run(Command::AuditExport {
1110                log: Log::Tenant(tenant),
1111                since: None,
1112            })
1113            .expect("exported");
1114        let actions: Vec<&str> = own
1115            .iter()
1116            .map(|r| r["action"].as_str().expect("action"))
1117            .collect();
1118        assert_eq!(actions, ["key_create"]);
1119    }
1120
1121    #[test]
1122    fn rekey_tenant_rotates_the_key_and_prints_nothing() {
1123        let s = setup();
1124        let id = s.create("acme");
1125        let tenant = TenantId::parse(&id).expect("id");
1126        let before = s.tenants().tenant_key_id_for_test(tenant);
1127        assert_eq!(
1128            s.run(Command::RekeyTenant { id: id.clone() })
1129                .expect("rekeyed"),
1130            Vec::<Value>::new()
1131        );
1132        assert_ne!(s.tenants().tenant_key_id_for_test(tenant), before);
1133        assert_eq!(
1134            tenant_error(s.run(Command::RekeyTenant {
1135                id: TenantId::generate().to_string()
1136            })),
1137            TenantError::NotFound
1138        );
1139        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1140        assert!(matches!(
1141            s.run(Command::RekeyTenant { id }),
1142            Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1143        ));
1144        drop(server);
1145    }
1146
1147    #[test]
1148    fn rekey_master_moves_the_data_to_the_new_master_key() {
1149        let mut s = setup();
1150        s.create("acme");
1151        let new_file = s.config.master_key.with_file_name("new.key");
1152        std::fs::write(&new_file, [0x5c; 32]).expect("new key");
1153        assert!(matches!(
1154            s.run(Command::RekeyMaster {
1155                new_master: s.config.master_key.clone()
1156            }),
1157            Err(CommandError::SameMasterKey)
1158        ));
1159        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1160        assert!(matches!(
1161            s.run(Command::RekeyMaster {
1162                new_master: new_file.clone()
1163            }),
1164            Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1165        ));
1166        drop(server);
1167        let out = s
1168            .run(Command::RekeyMaster {
1169                new_master: new_file.clone(),
1170            })
1171            .expect("rotated");
1172        let new_id = Key::load_master(&new_file).expect("key").id().to_string();
1173        assert_eq!(out.len(), 1);
1174        assert_eq!(out[0]["master_key_id"], new_id.as_str());
1175        assert!(
1176            out[0]["next"]
1177                .as_str()
1178                .is_some_and(|n| n.contains("destroy"))
1179        );
1180        assert!(matches!(
1181            s.run(Command::TenantList),
1182            Err(CommandError::WrongMasterKey)
1183        ));
1184        s.config.master_key = new_file;
1185        assert_eq!(s.run(Command::TenantList).expect("listed").len(), 1);
1186    }
1187
1188    mod check {
1189        use time::macros::datetime;
1190
1191        use super::*;
1192        use crate::ids::VersionId;
1193        use crate::store::{Examination, StoreError};
1194
1195        const NOW: OffsetDateTime = datetime!(2026-10-25 12:00 UTC);
1196        const HOUR: std::time::Duration = std::time::Duration::from_secs(3600);
1197
1198        fn sound() -> Examination {
1199            Examination {
1200                versions: 3,
1201                packed: 0,
1202                damaged: vec![],
1203                leftovers: vec!["a.tmp".to_owned()],
1204                due: 0,
1205                oldest_due_since: None,
1206                purges_since_key: 4,
1207                oldest_purge_since_key: Some(datetime!(2026-10-01 00:00 UTC)),
1208            }
1209        }
1210
1211        fn tenant(store: Result<Examination, Unexamined>, stopped: bool) -> TenantExamination {
1212            TenantExamination {
1213                id: TenantId([1; 16]),
1214                name: "acme".to_owned(),
1215                rotation_stopped: stopped,
1216                store,
1217            }
1218        }
1219
1220        #[test]
1221        fn leftovers_and_work_waiting_for_rotation_are_not_problems() {
1222            assert_eq!(
1223                problems(&tenant(Ok(sound()), false), HOUR, NOW, true),
1224                Vec::<String>::new()
1225            );
1226        }
1227
1228        #[test]
1229        fn each_problem_is_named() {
1230            let damaged = Examination {
1231                damaged: vec![VersionId([2; 16]), VersionId([3; 16])],
1232                ..sound()
1233            };
1234            assert_eq!(
1235                problems(&tenant(Ok(damaged), false), HOUR, NOW, true),
1236                [
1237                    "version 02020202020202020202020202020202 is missing or does not open",
1238                    "version 03030303030303030303030303030303 is missing or does not open",
1239                ],
1240                "one line per damaged version, in its order"
1241            );
1242            assert_eq!(
1243                problems(&tenant(Ok(sound()), true), HOUR, NOW, true),
1244                ["a key rotation was stopped part way; run rekey tenant again"],
1245                "a stopped rotation is named with the command that finishes it"
1246            );
1247            assert_eq!(
1248                problems(
1249                    &tenant(
1250                        Err(Unexamined::Open(OpenFailure::Refused(TenantError::Io))),
1251                        false
1252                    ),
1253                    HOUR,
1254                    NOW,
1255                    true,
1256                ),
1257                ["its catalog cannot be opened: Io"],
1258                "a refused catalog names the refusal, not a key"
1259            );
1260        }
1261
1262        /// Only a catalog its key does not open is a key problem; one the
1263        /// process may not open is named with its path and the system's
1264        /// error.
1265        #[test]
1266        fn a_key_problem_and_an_access_problem_are_told_apart() {
1267            let key = problems(
1268                &tenant(Err(Unexamined::Open(OpenFailure::WrongKey)), false),
1269                HOUR,
1270                NOW,
1271                true,
1272            );
1273            assert_eq!(key, ["its catalog does not open under its tenant key"]);
1274            let access = OpenFailure::Access {
1275                path: std::path::PathBuf::from("/srv/data/tenants/01/catalog.sqlite"),
1276                error: std::io::Error::from_raw_os_error(libc::EACCES),
1277            };
1278            assert_eq!(
1279                problems(
1280                    &tenant(Err(Unexamined::Open(access)), false),
1281                    HOUR,
1282                    NOW,
1283                    true
1284                ),
1285                [
1286                    "its catalog cannot be opened: /srv/data/tenants/01/catalog.sqlite: Permission denied (os error 13)"
1287                ]
1288            );
1289        }
1290
1291        /// Packed versions with no `[storage.parquet]` in the
1292        /// configuration are a problem, named with the setting; with the
1293        /// table, or with nothing packed, they are not.
1294        #[test]
1295        fn packed_versions_without_storage_parquet_are_a_problem() {
1296            let packed = Examination {
1297                packed: 2,
1298                ..sound()
1299            };
1300            assert_eq!(
1301                problems(&tenant(Ok(packed.clone()), false), HOUR, NOW, false),
1302                [
1303                    "2 version(s) are packed into Parquet files, and the configuration has no [storage.parquet]: serve refuses to start until the table is put back"
1304                ],
1305                "without the table"
1306            );
1307            assert_eq!(
1308                problems(&tenant(Ok(packed), false), HOUR, NOW, true),
1309                Vec::<String>::new(),
1310                "with the table"
1311            );
1312            assert_eq!(
1313                problems(&tenant(Ok(sound()), false), HOUR, NOW, false),
1314                Vec::<String>::new(),
1315                "nothing packed"
1316            );
1317        }
1318
1319        /// A Store that opened and whose objects the storage did not read
1320        /// is named as that, with the error's kind and the system's error
1321        /// number when there is one, and never as a catalog that does not
1322        /// open; any other failed examination says so with its error.
1323        #[test]
1324        fn objects_that_cannot_be_read_are_told_apart_from_a_catalog() {
1325            let unread = |kind, os_error| {
1326                problems(
1327                    &tenant(
1328                        Err(Unexamined::Examine(ExamineError::Objects {
1329                            kind,
1330                            os_error,
1331                        })),
1332                        false,
1333                    ),
1334                    HOUR,
1335                    NOW,
1336                    true,
1337                )
1338            };
1339            assert_eq!(
1340                unread(std::io::ErrorKind::PermissionDenied, Some(libc::EACCES)),
1341                ["its objects cannot be read: permission denied (os error 13)"],
1342                "a file the process may not read"
1343            );
1344            assert_eq!(
1345                unread(std::io::ErrorKind::TimedOut, None),
1346                ["its objects cannot be read: timed out"],
1347                "a service's failure has a kind and no system error number"
1348            );
1349            assert_eq!(
1350                problems(
1351                    &tenant(
1352                        Err(Unexamined::Examine(ExamineError::Failed(StoreError::Io))),
1353                        false
1354                    ),
1355                    HOUR,
1356                    NOW,
1357                    true,
1358                ),
1359                ["it could not be examined: Io"],
1360                "any other failure, with its error"
1361            );
1362        }
1363
1364        #[test]
1365        fn a_sweep_is_stalled_only_once_a_version_waited_longer_than_the_interval() {
1366            let waited = |since| Examination {
1367                due: 1,
1368                oldest_due_since: Some(since),
1369                ..sound()
1370            };
1371            let ok = waited(datetime!(2026-10-25 11:00 UTC));
1372            assert_eq!(
1373                problems(&tenant(Ok(ok), false), HOUR, NOW, true),
1374                Vec::<String>::new()
1375            );
1376            let stalled = waited(datetime!(2026-10-25 10:59:59 UTC));
1377            assert_eq!(
1378                problems(&tenant(Ok(stalled), false), HOUR, NOW, true),
1379                [
1380                    "1 version(s) due for purge, the oldest longer than purge_interval: the sweep is not keeping up"
1381                ],
1382                "one second past purge_interval is a stalled sweep"
1383            );
1384        }
1385
1386        #[test]
1387        fn check_reports_each_tenant_and_a_summary_and_exits_1_on_a_problem() {
1388            let s = setup();
1389            let id = s.create("acme");
1390            let out = s.run(Command::Check).expect("nothing wrong");
1391            assert_eq!(out.len(), 2);
1392            assert_eq!(out[0]["tenant"], id.as_str());
1393            assert_eq!(out[0]["problems"], json!([]));
1394            assert_eq!(out[1]["tenants"], 1);
1395            assert_eq!(out[1]["problems"], 0);
1396
1397            let tenant = TenantId::parse(&id).expect("id");
1398            let version = {
1399                let tenants = s.tenants();
1400                let store = tenants.store(tenant).expect("store");
1401                let (found, body) = vcon(UUID, "kept");
1402                let Ok(crate::store::Created::New(v)) =
1403                    store.create(&Caller { key: KeyId([1; 8]) }, &found, &body)
1404                else {
1405                    panic!("stored")
1406                };
1407                v
1408            };
1409            let objects = s.config.data_dir.join("tenants").join(&id).join("objects");
1410            std::fs::remove_file(objects.join(version.to_string())).expect("remove");
1411            std::fs::write(objects.join(format!("{version}.tmp")), b"x").expect("leftover");
1412            let mut printed = Vec::new();
1413            let found = run(&s.config, &Command::Check, &mut printed);
1414            assert!(
1415                matches!(found, Err(CommandError::CheckFound(1))),
1416                "{found:?}"
1417            );
1418            let lines: Vec<Value> = String::from_utf8(printed)
1419                .expect("text")
1420                .lines()
1421                .map(|l| serde_json::from_str(l).expect("json"))
1422                .collect();
1423            assert_eq!(lines[0]["damaged"], json!([version.to_string()]));
1424            assert_eq!(lines[0]["leftovers"], json!([format!("{version}.tmp")]));
1425            assert_eq!(lines[1]["problems"], 1);
1426            assert!(
1427                objects.join(format!("{version}.tmp")).exists(),
1428                "check reports leftovers; it does not remove them"
1429            );
1430            let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1431            assert!(matches!(
1432                s.run(Command::Check),
1433                Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1434            ));
1435            drop(server);
1436        }
1437    }
1438
1439    /// The commands that run alone are refused while a server holds the
1440    /// data directory; the others run beside it.
1441    #[test]
1442    fn beside_a_server_only_the_beside_commands_run() {
1443        let s = setup();
1444        let tenant = s.create("acme");
1445        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1446        for command in [
1447            Command::TenantCreate {
1448                name: "beta".to_owned(),
1449                retention: None,
1450            },
1451            Command::TenantRemove {
1452                id: tenant.clone(),
1453                force: true,
1454                override_holds: false,
1455            },
1456        ] {
1457            assert!(
1458                matches!(
1459                    s.run(command.clone()),
1460                    Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1461                ),
1462                "{command:?}"
1463            );
1464        }
1465        s.run(Command::TenantList).expect("beside");
1466        s.run(Command::TenantSetRetention {
1467            id: tenant.clone(),
1468            retention: "1y".to_owned(),
1469        })
1470        .expect("beside");
1471        s.run(Command::KeyList { tenant: None }).expect("beside");
1472        let made = s
1473            .run(Command::KeyCreate {
1474                owner: Owner::Operator,
1475                label: "l".to_owned(),
1476            })
1477            .expect("beside");
1478        s.run(Command::KeyRevoke {
1479            id: made[0]["id"].as_str().expect("an id").to_owned(),
1480        })
1481        .expect("beside");
1482        s.run(Command::AuditExport {
1483            log: Log::Admin,
1484            since: None,
1485        })
1486        .expect("beside");
1487        s.run(Command::Backup {
1488            to: s._dir.path().join("backup"),
1489        })
1490        .expect("beside");
1491        drop(server);
1492        assert_eq!(s.run(Command::TenantList).expect("listed").len(), 1);
1493    }
1494
1495    /// A command beside a server opens without recovery, so a create the
1496    /// server has in progress stays; one that runs alone recovers.
1497    #[test]
1498    fn only_a_command_that_runs_alone_recovers() {
1499        let s = setup();
1500        s.create("acme");
1501        let in_progress = TenantId::generate().to_string();
1502        std::fs::create_dir(s.config.data_dir.join("tenants").join(&in_progress))
1503            .expect("a directory with no row");
1504        s.run(Command::TenantList).expect("listed");
1505        s.run(Command::KeyList { tenant: None }).expect("listed");
1506        assert!(
1507            s.tenant_dirs().contains(&in_progress),
1508            "left by beside commands"
1509        );
1510        s.create("beta");
1511        assert!(
1512            !s.tenant_dirs().contains(&in_progress),
1513            "removed by an alone command"
1514        );
1515    }
1516
1517    #[test]
1518    fn a_wrong_master_key_is_named() {
1519        let s = setup();
1520        s.create("acme");
1521        std::fs::write(&s.config.master_key, [0x5b; 32]).expect("another key");
1522        let error = s.run(Command::TenantList).expect_err("refused");
1523        assert!(matches!(error, CommandError::WrongMasterKey), "{error:?}");
1524        assert!(error.to_string().contains("master key"), "{error}");
1525        std::fs::write(&s.config.master_key, [0x5a; 31]).expect("short key");
1526        let error = s.run(Command::TenantList).expect_err("refused");
1527        assert!(matches!(error, CommandError::MasterKey(_)), "{error:?}");
1528    }
1529
1530    #[test]
1531    fn backup_prints_what_it_copied_and_check_finds_the_copy_whole() {
1532        let s = setup();
1533        s.create("acme");
1534        s.create("globex");
1535        let to = s._dir.path().join("backup");
1536        let out = s.run(Command::Backup { to: to.clone() }).expect("backs up");
1537        assert_eq!(
1538            out,
1539            [serde_json::json!({ "tenants": 2, "versions": 0, "tries": 1 })]
1540        );
1541        let copy = Setup {
1542            _dir: tempfile::tempdir().expect("temp dir"),
1543            config: Config {
1544                data_dir: to,
1545                ..s.config.clone()
1546            },
1547        };
1548        let checked = copy.run(Command::Check).expect("check finds no problem");
1549        assert_eq!(checked.last().expect("a summary")["problems"], 0);
1550        let names: Vec<_> = copy
1551            .run(Command::TenantList)
1552            .expect("lists")
1553            .iter()
1554            .map(|t| t["name"].clone())
1555            .collect();
1556        assert_eq!(names, ["acme", "globex"]);
1557    }
1558
1559    #[test]
1560    fn a_backup_path_already_there_is_refused() {
1561        let s = setup();
1562        let to = s._dir.path().join("backup");
1563        std::fs::create_dir(&to).expect("dir");
1564        let error = s.run(Command::Backup { to }).expect_err("refused");
1565        assert!(
1566            matches!(error, CommandError::Backup(BackupError::Exists)),
1567            "{error:?}"
1568        );
1569    }
1570
1571    /// Words the message of `error` must hold to say what went wrong. The
1572    /// matches have no catch-all arm, so a new error fails to compile here
1573    /// until its words are given.
1574    fn says(error: &CommandError) -> &'static str {
1575        match error {
1576            CommandError::DataDir(_) => "the data directory is in use",
1577            CommandError::MasterKey(_) => "the master key cannot be read",
1578            // The step, the file and the error under it, all three.
1579            CommandError::Open(_) => "opening /srv/data/server.sqlite failed: disk I/O error",
1580            CommandError::WrongMasterKey => "is not the one server.sqlite was made under",
1581            CommandError::BadPeriod(_) => "\"7w\" is not a period",
1582            CommandError::UnknownKey => "no key has this id",
1583            CommandError::Output(_) => "the output could not be written",
1584            CommandError::SameMasterKey => "already in use",
1585            CommandError::CheckFound(_) => "check found 2 problem(s)",
1586            CommandError::Storage(_) => "the storage cannot be used: bucket \"vcon\"",
1587            CommandError::BadLevel(_) => "\"most\" is not a metadata level",
1588            CommandError::BadTime(_) => "\"yesterday\" is not an RFC 3339 time",
1589            CommandError::BadTarget(_) => "\"s3://\" is not a directory or an s3://bucket/prefix",
1590            CommandError::NoS3 => "[export.s3] or [storage.s3]",
1591            CommandError::Export(_) => "another export wrote",
1592            CommandError::Move { .. } => "running storage move-objects again moves the rest",
1593            CommandError::Backup(e) => match e {
1594                BackupError::Exists => "already exists",
1595                BackupError::KeptChanging => "try again",
1596                BackupError::Io => "could not be written",
1597                BackupError::AuditFailed => "audit record",
1598            },
1599            CommandError::Tenant(e) => match e {
1600                TenantError::NotFound => "no tenant",
1601                TenantError::NameTaken => "another tenant has this name",
1602                TenantError::BadName => "a tenant name is",
1603                TenantError::HoldsVcons => "--force",
1604                TenantError::UnderHold => "--override-holds",
1605                TenantError::LimitReached => "rekey master",
1606                TenantError::AuditFailed => "audit record",
1607                TenantError::Closing => "being removed",
1608                TenantError::CliOnly => "command line",
1609                TenantError::BadScopes => "scopes",
1610                // Display folds a registry made under another master key
1611                // into WrongMasterKey's message.
1612                TenantError::WrongKey => "is not the one server.sqlite was made under",
1613                TenantError::Io => "could not be read or written",
1614            },
1615        }
1616    }
1617
1618    /// With the objects kept in the data directory there is no bucket to
1619    /// move them to: the command is refused, naming the setting.
1620    #[test]
1621    fn storage_move_objects_needs_a_bucket() {
1622        let s = setup();
1623        s.create("acme");
1624        let error = s.run(Command::StorageMoveObjects).expect_err("refused");
1625        assert!(
1626            error.to_string().contains("storage.objects = \"s3\""),
1627            "names the setting: {error}"
1628        );
1629    }
1630
1631    /// Against the test service: a data directory's objects move to the
1632    /// bucket, byte for byte; the vCon then reads back from the bucket, no
1633    /// object file is left, and a second run moves nothing.
1634    #[cfg(feature = "backend-tests")]
1635    #[test]
1636    fn storage_move_objects_moves_each_tenants_objects_to_the_bucket() {
1637        use crate::backend::Objects;
1638        let s = setup();
1639        let id = s.create("acme");
1640        store_one(&s, &id);
1641        let tenant = TenantId::parse(&id).expect("an id");
1642        let local = s.config.data_dir.join("tenants").join(&id).join("objects");
1643        let files = || std::fs::read_dir(&local).expect("listed").count();
1644        assert_eq!(files(), 1, "one object file");
1645        let (bucket, mut settings) = crate::backend::s3::remote::bucket(false);
1646        let secret = crate::backend::s3::remote::secret_file();
1647        settings.secret_access_key_file = Some(secret.1.clone());
1648        let mut config = s.config.clone();
1649        config.storage = crate::config::Storage {
1650            objects: crate::config::ObjectStore::S3(settings),
1651            parquet: None,
1652        };
1653        let moving = |config: &Config| -> Vec<Value> {
1654            let mut out = Vec::new();
1655            run(config, &Command::StorageMoveObjects, &mut out).expect("moved");
1656            String::from_utf8(out)
1657                .expect("text")
1658                .lines()
1659                .map(|l| serde_json::from_str(l).expect("a JSON line"))
1660                .collect()
1661        };
1662        assert_eq!(
1663            moving(&config),
1664            [
1665                json!({"tenant": id, "moved": 1, "missing": 0}),
1666                json!({"moved": 1})
1667            ],
1668            "one object moved, and nothing missing"
1669        );
1670        assert_eq!(files(), 0, "no object file is left");
1671        assert_eq!(
1672            bucket.tenant(tenant).list().expect("listed").len(),
1673            1,
1674            "the object is in the bucket"
1675        );
1676        let tenants = Tenants::open_beside_in(
1677            &config.data_dir,
1678            Key::load_master(&config.master_key).expect("master"),
1679            Arc::new(OsFs),
1680            Arc::new(SystemClock),
1681            crate::backend::ObjectPlace::S3(bucket.clone()),
1682        )
1683        .expect("opens");
1684        let (_, body) = vcon(UUID, "exported");
1685        assert_eq!(
1686            tenants
1687                .store(tenant)
1688                .expect("its store")
1689                .get(&Caller { key: KeyId([7; 8]) }, UUID.parse().expect("uuid"))
1690                .expect("reads")
1691                .body,
1692            body,
1693            "the vCon reads back from the bucket"
1694        );
1695        drop(tenants);
1696        assert_eq!(
1697            moving(&config),
1698            [
1699                json!({"tenant": id, "moved": 0, "missing": 0}),
1700                json!({"moved": 0})
1701            ],
1702            "a second run moves nothing"
1703        );
1704    }
1705
1706    /// One vCon stored in the tenant `id`.
1707    fn store_one(s: &Setup, id: &str) {
1708        let store = s
1709            .tenants()
1710            .store(TenantId::parse(id).expect("an id"))
1711            .expect("the store");
1712        let (found, body) = vcon(UUID, "exported");
1713        store
1714            .create(&Caller { key: KeyId([7; 8]) }, &found, &body)
1715            .expect("stored");
1716    }
1717
1718    #[test]
1719    fn tenant_set_metadata_prints_the_level_and_list_shows_it() {
1720        let s = setup();
1721        let id = s.create("acme");
1722        let out = s
1723            .run(Command::TenantSetMetadata {
1724                id: id.clone(),
1725                metadata: "counts".to_owned(),
1726            })
1727            .expect("set");
1728        assert_eq!(
1729            out,
1730            [json!({ "id": id, "metadata": "counts" })],
1731            "the command prints the tenant and its level"
1732        );
1733        assert_eq!(
1734            s.run(Command::TenantList).expect("listed")[0]["metadata"],
1735            "counts",
1736            "the list shows the level"
1737        );
1738        assert!(
1739            matches!(
1740                s.run(Command::TenantSetMetadata {
1741                    id: id.clone(),
1742                    metadata: "most".to_owned(),
1743                }),
1744                Err(CommandError::BadLevel(text)) if text == "most"
1745            ),
1746            "a level that is not one is refused, naming it"
1747        );
1748        assert_eq!(
1749            tenant_error(s.run(Command::TenantSetMetadata {
1750                id: TenantId::generate().to_string(),
1751                metadata: "full".to_owned(),
1752            })),
1753            TenantError::NotFound,
1754            "a tenant id no tenant has is not found"
1755        );
1756    }
1757
1758    /// The export runs beside a server, at the tenant's level, and prints
1759    /// one line; it is refused beside a command that runs alone.
1760    #[test]
1761    fn export_parquet_runs_beside_a_server_and_prints_what_it_wrote() {
1762        let s = setup();
1763        let id = s.create("acme");
1764        store_one(&s, &id);
1765        s.run(Command::TenantSetMetadata {
1766            id: id.clone(),
1767            metadata: "counts".to_owned(),
1768        })
1769        .expect("set");
1770        let to = s._dir.path().join("export");
1771        let export = Command::ExportParquet {
1772            tenant: id.clone(),
1773            to: to.display().to_string(),
1774            since: Some("2000-01-01T00:00:00Z".to_owned()),
1775            until: None,
1776        };
1777        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1778        let out = s.run(export.clone()).expect("exports beside a server");
1779        drop(server);
1780        assert_eq!(out.len(), 1, "{out:?}");
1781        let line = &out[0];
1782        let manifest = format!("tenant={id}/manifest/00000000000000000001.json");
1783        assert_eq!(line["tenant"], id, "the line names the tenant");
1784        assert_eq!(
1785            line["metadata"], "counts",
1786            "the export is at the tenant's level"
1787        );
1788        assert_eq!(
1789            line["to"],
1790            to.display().to_string(),
1791            "the line names the target"
1792        );
1793        assert_eq!(line["manifest"], manifest, "the line names the manifest");
1794        assert_eq!(line["seq"], 1, "the first export to the target");
1795        assert_eq!(line["files"], 1, "one store date, one file");
1796        assert_eq!(line["rows"], 1, "the one vCon stored");
1797        assert_eq!(line["skipped"], 0, "nothing was purged during the run");
1798        assert_eq!(line["unread"], 0, "every body was read");
1799        let m: Value =
1800            serde_json::from_slice(&std::fs::read(to.join(&manifest)).expect("the manifest"))
1801                .expect("JSON");
1802        assert_eq!(m["bytes"], line["bytes"], "the bytes the manifest names");
1803        assert_eq!(
1804            m["since"], "2000-01-01T00:00:00Z",
1805            "the manifest records --since"
1806        );
1807        let alone = datadir::lock(&s.config.data_dir, Sharing::Alone).expect("alone");
1808        assert!(
1809            matches!(
1810                s.run(export),
1811                Err(CommandError::DataDir(LockError::InUse(Sharing::Beside)))
1812            ),
1813            "an export does not run beside a command that runs alone"
1814        );
1815        drop(alone);
1816    }
1817
1818    #[test]
1819    fn export_parquet_refuses_a_bad_time_target_or_tenant() {
1820        let s = setup();
1821        let id = s.create("acme");
1822        let export = |to: &str, since: Option<&str>, until: Option<&str>| Command::ExportParquet {
1823            tenant: id.clone(),
1824            to: to.to_owned(),
1825            since: since.map(str::to_owned),
1826            until: until.map(str::to_owned),
1827        };
1828        let dir = s._dir.path().join("out").display().to_string();
1829        assert!(
1830            matches!(
1831                s.run(export(&dir, Some("yesterday"), None)),
1832                Err(CommandError::BadTime(t)) if t == "yesterday"
1833            ),
1834            "a word that is not an RFC 3339 time is refused, naming it"
1835        );
1836        assert!(
1837            matches!(
1838                s.run(export(&dir, None, Some("2026-13-01T00:00:00Z"))),
1839                Err(CommandError::BadTime(_))
1840            ),
1841            "a month 13 is not a time"
1842        );
1843        assert!(
1844            matches!(
1845                s.run(export("s3://", None, None)),
1846                Err(CommandError::BadTarget(t)) if t == "s3://"
1847            ),
1848            "an s3:// URL with no bucket is refused, naming it"
1849        );
1850        assert!(
1851            matches!(
1852                s.run(export("s3://reports/vcon", None, None)),
1853                Err(CommandError::NoS3)
1854            ),
1855            "no S3 service is configured"
1856        );
1857        assert_eq!(
1858            tenant_error(s.run(Command::ExportParquet {
1859                tenant: TenantId::generate().to_string(),
1860                to: dir.clone(),
1861                since: None,
1862                until: None,
1863            })),
1864            TenantError::NotFound,
1865            "an unknown tenant is not found"
1866        );
1867        assert!(
1868            !std::path::Path::new(&dir).exists(),
1869            "a refused export writes nothing"
1870        );
1871    }
1872
1873    #[test]
1874    fn each_error_says_what_went_wrong() {
1875        let tenant = [
1876            TenantError::NotFound,
1877            TenantError::NameTaken,
1878            TenantError::BadName,
1879            TenantError::HoldsVcons,
1880            TenantError::UnderHold,
1881            TenantError::LimitReached,
1882            TenantError::AuditFailed,
1883            TenantError::Closing,
1884            TenantError::CliOnly,
1885            TenantError::BadScopes,
1886            TenantError::WrongKey,
1887            TenantError::Io,
1888        ]
1889        .map(CommandError::Tenant);
1890        let backup = [
1891            BackupError::Exists,
1892            BackupError::KeptChanging,
1893            BackupError::Io,
1894            BackupError::AuditFailed,
1895        ]
1896        .map(CommandError::Backup);
1897        let rest = [
1898            CommandError::DataDir(LockError::InUse(Sharing::Alone)),
1899            CommandError::MasterKey(crate::envelope::KeyError::WrongSize),
1900            CommandError::Open(RegistryError::failed(
1901                tenants::OpenStep::OpenCatalog,
1902                std::path::PathBuf::from("/srv/data/server.sqlite"),
1903                io::Error::other("disk I/O error"),
1904            )),
1905            CommandError::WrongMasterKey,
1906            CommandError::BadPeriod("7w".to_owned()),
1907            CommandError::UnknownKey,
1908            CommandError::Output(io::Error::other("closed")),
1909            CommandError::SameMasterKey,
1910            CommandError::CheckFound(2),
1911            CommandError::Storage(StorageError(
1912                "bucket \"vcon\" cannot be reached or read".to_owned(),
1913            )),
1914            CommandError::BadLevel("most".to_owned()),
1915            CommandError::BadTime("yesterday".to_owned()),
1916            CommandError::BadTarget("s3://".to_owned()),
1917            CommandError::NoS3,
1918            CommandError::Export(crate::export::ExportError::Raced("m".to_owned())),
1919            CommandError::Move {
1920                tenant: TenantId([1; 16]),
1921                why: "the bucket could not be written".to_owned(),
1922            },
1923        ];
1924        for error in tenant.into_iter().chain(backup).chain(rest) {
1925            let text = error.to_string();
1926            assert!(text.contains(says(&error)), "{error:?}: {text}");
1927        }
1928    }
1929}