Skip to main content

kopiur_api/
repository.rs

1//! The `Repository` CRD — a namespaced kopia repository. ADR-0003 §3.1.
2
3use crate::backend::Backend;
4use crate::common::{CacheDefaults, CatalogBounds, CreateBehavior, Encryption};
5use crate::maintenance::RepositoryMaintenanceSpec;
6use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;
7use kube::CustomResource;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11/// A kopia repository owned by one namespace: credentials, backend, encryption,
12/// and optional catalog-materialization bounds. Many `BackupConfig`s / `Restore`s
13/// reference one. ADR §3.1.
14#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
15#[kube(
16    group = "kopiur.home-operations.com",
17    version = "v1alpha1",
18    kind = "Repository",
19    namespaced,
20    status = "RepositoryStatus",
21    shortname = "kopiarepo",
22    category = "kopiur",
23    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
24    printcolumn = r#"{"name":"Backend","type":"string","jsonPath":".status.backend"}"#,
25    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
26)]
27#[serde(rename_all = "camelCase")]
28pub struct RepositorySpec {
29    /// Exactly one backend, enforced at the type level by the `Backend` enum. ADR §3.1.
30    pub backend: Backend,
31    /// Repository password, always a Secret reference. A sub-object so future
32    /// rotation fields slot in without API breakage. ADR §3.1/§4.11.
33    pub encryption: Encryption,
34    /// What to do when the repository does not yet exist. Absent/disabled means
35    /// it must already exist; enabled means the operator creates it with the
36    /// given encryption/splitter/hash algorithms. ADR §3.1.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub create: Option<CreateBehavior>,
39    /// Cache sizing inherited by `Backup`/`Restore` movers unless overridden. ADR §3.1.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub cache_defaults: Option<CacheDefaults>,
42    /// Bounds materialization of `origin: discovered` `Backup` CRs from the kopia
43    /// catalog, keeping etcd footprint sane for large repositories. ADR §3.1.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub catalog: Option<CatalogBounds>,
46    /// Maintenance control. Default-managed: when absent or `enabled: true`, the
47    /// reconciler creates and owns a `Maintenance` CR for this repository in this
48    /// namespace. ADR §3.1/§3.7.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub maintenance: Option<RepositoryMaintenanceSpec>,
51}
52
53/// Lifecycle phase of a repository. ADR §3.1 status.
54///
55/// A freshly admitted CR starts in the `#[default]` [`RepositoryPhase::Pending`]:
56///
57/// ```
58/// use kopiur_api::repository::RepositoryPhase;
59///
60/// assert_eq!(RepositoryPhase::default(), RepositoryPhase::Pending);
61/// // Serializes as a bare string (closed unit enum).
62/// assert_eq!(
63///     serde_json::to_value(RepositoryPhase::Ready).unwrap(),
64///     serde_json::json!("Ready")
65/// );
66/// ```
67#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default, JsonSchema)]
68pub enum RepositoryPhase {
69    /// Accepted by the API server but not yet reconciled.
70    #[default]
71    Pending,
72    /// Connecting to (or creating) the kopia repository.
73    Initializing,
74    /// Connected and healthy.
75    Ready,
76    /// Reachable, but a sub-operation (e.g. maintenance) is failing; see conditions.
77    Degraded,
78    /// Connect/create failed; see conditions for the actionable reason.
79    Failed,
80}
81
82impl crate::common::PhaseLabel for RepositoryPhase {
83    const ALL: &'static [Self] = &[
84        Self::Pending,
85        Self::Initializing,
86        Self::Ready,
87        Self::Degraded,
88        Self::Failed,
89    ];
90    fn label(&self) -> &'static str {
91        match self {
92            Self::Pending => "Pending",
93            Self::Initializing => "Initializing",
94            Self::Ready => "Ready",
95            Self::Degraded => "Degraded",
96            Self::Failed => "Failed",
97        }
98    }
99}
100
101/// Observed state of a [`Repository`]. Carries resolved values pinned by the
102/// reconciler. ADR §3.1 status.
103#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default, JsonSchema)]
104#[serde(rename_all = "camelCase")]
105pub struct RepositoryStatus {
106    /// Current lifecycle phase.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub phase: Option<RepositoryPhase>,
109    /// `metadata.generation` of the `spec` last reconciled; drives staleness detection.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub observed_generation: Option<i64>,
112    /// Kopia repository unique ID.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub unique_id: Option<String>,
115    /// Mirror of `spec.backend` discriminant for the print column.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub backend: Option<String>,
118    /// Repository size and snapshot counts from the last catalog scan.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub storage_stats: Option<StorageStats>,
121    /// Catalog-materialization status (how many discovered `Backup`s, last refresh).
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub catalog: Option<CatalogStatus>,
124    /// Standard Kubernetes conditions (e.g. `Connected`, `MaintenanceOwned`). ADR §3.1.
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub conditions: Vec<Condition>,
127}
128
129/// Aggregate repository storage figures from the last catalog scan. ADR §3.1 status.
130#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default, JsonSchema)]
131#[serde(rename_all = "camelCase")]
132pub struct StorageStats {
133    /// Total snapshots present in the repository (across all identities).
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub snapshot_count: Option<i64>,
136    /// Human-readable total on-disk size (e.g. `412Gi`).
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub total_size: Option<String>,
139    /// RFC 3339 timestamp these stats were last observed.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub last_observed_at: Option<String>,
142}
143
144/// Status of catalog materialization for `origin: discovered` `Backup` CRs. ADR §3.1 status.
145#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default, JsonSchema)]
146#[serde(rename_all = "camelCase")]
147pub struct CatalogStatus {
148    /// How many `Backup` CRs were materialized from the catalog scan.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub discovered_backup_count: Option<i64>,
151    /// RFC 3339 timestamp of the last catalog refresh.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub last_refresh_at: Option<String>,
154}