From 27718f2a72ff6f188f509556abbf28663ac16680 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 19 May 2022 09:43:09 +0200 Subject: [PATCH] api-types: add PruneJobConfig Signed-off-by: Wolfgang Bumiller --- pbs-api-types/src/datastore.rs | 68 ++++-------- pbs-api-types/src/jobs.rs | 182 +++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 46 deletions(-) diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs index c96521cb..9331a8a5 100644 --- a/pbs-api-types/src/datastore.rs +++ b/pbs-api-types/src/datastore.rs @@ -157,52 +157,6 @@ pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema = .minimum(1) .schema(); -#[api( - properties: { - "keep-last": { - schema: PRUNE_SCHEMA_KEEP_LAST, - optional: true, - }, - "keep-hourly": { - schema: PRUNE_SCHEMA_KEEP_HOURLY, - optional: true, - }, - "keep-daily": { - schema: PRUNE_SCHEMA_KEEP_DAILY, - optional: true, - }, - "keep-weekly": { - schema: PRUNE_SCHEMA_KEEP_WEEKLY, - optional: true, - }, - "keep-monthly": { - schema: PRUNE_SCHEMA_KEEP_MONTHLY, - optional: true, - }, - "keep-yearly": { - schema: PRUNE_SCHEMA_KEEP_YEARLY, - optional: true, - }, - } -)] -#[derive(Serialize, Deserialize, Default)] -#[serde(rename_all = "kebab-case")] -/// Common pruning options -pub struct PruneOptions { - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_last: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_hourly: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_daily: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_weekly: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_monthly: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub keep_yearly: Option, -} - #[api] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] @@ -705,6 +659,28 @@ impl BackupNamespace { path } } + + /// Check whether this namespace contains another namespace. + /// + /// If so, the depth is returned. + /// + /// Example: + /// ``` + /// # use pbs_api_types::BackupNamespace; + /// let main: BackupNamespace = "a/b".parse().unwrap(); + /// let sub: BackupNamespace = "a/b/c/d".parse().unwrap(); + /// let other: BackupNamespace = "x/y".parse().unwrap(); + /// assert_eq!(main.contains(&main), Some(0)); + /// assert_eq!(main.contains(&sub), Some(2)); + /// assert_eq!(sub.contains(&main), None); + /// assert_eq!(main.contains(&other), None); + /// ``` + pub fn contains(&self, other: &BackupNamespace) -> Option { + other + .inner + .strip_prefix(&self.inner[..]) + .map(|suffix| suffix.len()) + } } impl fmt::Display for BackupNamespace { diff --git a/pbs-api-types/src/jobs.rs b/pbs-api-types/src/jobs.rs index c65a6085..45a2c4f2 100644 --- a/pbs-api-types/src/jobs.rs +++ b/pbs-api-types/src/jobs.rs @@ -535,3 +535,185 @@ pub struct SyncJobStatus { #[serde(flatten)] pub status: JobScheduleStatus, } + +/// These are used separately without `ns`/`max-depth` sometimes in the API, specifically in the API +/// call to prune a specific group, where `max-depth` makes no sense. +#[api( + properties: { + "keep-last": { + schema: crate::PRUNE_SCHEMA_KEEP_LAST, + optional: true, + }, + "keep-hourly": { + schema: crate::PRUNE_SCHEMA_KEEP_HOURLY, + optional: true, + }, + "keep-daily": { + schema: crate::PRUNE_SCHEMA_KEEP_DAILY, + optional: true, + }, + "keep-weekly": { + schema: crate::PRUNE_SCHEMA_KEEP_WEEKLY, + optional: true, + }, + "keep-monthly": { + schema: crate::PRUNE_SCHEMA_KEEP_MONTHLY, + optional: true, + }, + "keep-yearly": { + schema: crate::PRUNE_SCHEMA_KEEP_YEARLY, + optional: true, + }, + } +)] +#[derive(Serialize, Deserialize, Default, Updater)] +#[serde(rename_all = "kebab-case")] +/// Common pruning options +pub struct KeepOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_last: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_hourly: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_daily: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_weekly: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_monthly: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub keep_yearly: Option, +} + +impl KeepOptions { + pub fn keeps_something(&self) -> bool { + self.keep_last.unwrap_or(0) + + self.keep_hourly.unwrap_or(0) + + self.keep_daily.unwrap_or(0) + + self.keep_monthly.unwrap_or(0) + + self.keep_yearly.unwrap_or(0) + > 0 + } +} + +#[api( + properties: { + keep: { + type: KeepOptions, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + "max-depth": { + schema: NS_MAX_DEPTH_REDUCED_SCHEMA, + optional: true, + }, + } +)] +#[derive(Serialize, Deserialize, Default, Updater)] +#[serde(rename_all = "kebab-case")] +/// Common pruning options +pub struct PruneJobOptions { + #[serde(flatten)] + pub keep: KeepOptions, + + /// The (optional) recursion depth + #[serde(skip_serializing_if = "Option::is_none")] + pub max_depth: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ns: Option, +} + +impl PruneJobOptions { + pub fn keeps_something(&self) -> bool { + self.keep.keeps_something() + } + + pub fn acl_path<'a>(&'a self, store: &'a str) -> Vec<&'a str> { + match &self.ns { + Some(ns) => ns.acl_path(store), + None => vec!["datastore", store], + } + } +} + +#[api( + properties: { + disable: { + type: Boolean, + optional: true, + default: false, + }, + id: { + schema: JOB_ID_SCHEMA, + }, + store: { + schema: DATASTORE_SCHEMA, + }, + schedule: { + schema: PRUNE_SCHEDULE_SCHEMA, + optional: true, + }, + comment: { + optional: true, + schema: SINGLE_LINE_COMMENT_SCHEMA, + }, + options: { + type: PruneJobOptions, + }, + }, +)] +#[derive(Deserialize, Serialize, Updater)] +#[serde(rename_all = "kebab-case")] +/// Prune configuration. +pub struct PruneJobConfig { + /// unique ID to address this job + #[updater(skip)] + pub id: String, + + pub store: String, + + /// Disable this job. + #[serde(default, skip_serializing_if = "is_false")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub disable: bool, + + pub schedule: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, + + #[serde(flatten)] + pub options: PruneJobOptions, +} + +impl PruneJobConfig { + pub fn acl_path(&self) -> Vec<&str> { + self.options.acl_path(&self.store) + } +} + +fn is_false(b: &bool) -> bool { + !b +} + +#[api( + properties: { + config: { + type: PruneJobConfig, + }, + status: { + type: JobScheduleStatus, + }, + }, +)] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Status of prune job +pub struct PruneJobStatus { + #[serde(flatten)] + pub config: PruneJobConfig, + #[serde(flatten)] + pub status: JobScheduleStatus, +}