From 1fa6f2c6ede5088f6e597a1e0605e0b9e060f39b Mon Sep 17 00:00:00 2001 From: nikoshet Date: Wed, 16 Oct 2024 19:21:50 +0300 Subject: [PATCH] Update --- Cargo.lock | 1 + Cargo.toml | 1 + src/backup/backup_operator.rs | 199 +++++++++--------- src/k8s_ops/mod.rs | 1 + src/k8s_ops/persistent_volume_claims.rs | 177 ++++++++++------ .../persistent_volume_claims_payload.rs | 66 ++++++ src/k8s_ops/volume_snapshot_contents.rs | 122 ++++++----- src/k8s_ops/volume_snapshots.rs | 165 +++++++++------ src/main.rs | 65 +++++- src/restore/restore_operator.rs | 171 +++++++-------- 10 files changed, 585 insertions(+), 383 deletions(-) create mode 100644 src/k8s_ops/persistent_volume_claims_payload.rs diff --git a/Cargo.lock b/Cargo.lock index cf69239..a2fd999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1747,6 +1747,7 @@ name = "pvc-snapshotter" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "aws-config", "aws-sdk-ec2", "clap", diff --git a/Cargo.toml b/Cargo.toml index b854da7..9ec7099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.89" +async-trait = "0.1.83" aws-config = "1.5.7" aws-sdk-ec2 = "1.75.0" clap = { version = "4.5.20", features = ["derive"] } diff --git a/src/backup/backup_operator.rs b/src/backup/backup_operator.rs index 45381fa..bb15164 100644 --- a/src/backup/backup_operator.rs +++ b/src/backup/backup_operator.rs @@ -1,4 +1,13 @@ +use crate::{ + aws_ops::ebs::create_ebs_client, + k8s_ops::{ + persistent_volume_claims::{check_if_pvc_exists, get_pvcs_available}, + volume_snapshots::{wait_untill_snapshot_is_ready, VolumeSnapshotOperator}, + }, +}; use anyhow::{bail, Result}; +use async_trait::async_trait; +use k8s_openapi::api::core::v1::PersistentVolumeClaim; use kube::{api::PostParams, Api, Client}; use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::{ volumesnapshotcontents::VolumeSnapshotContent, @@ -6,91 +15,55 @@ use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::{ }; use tracing::info; -use crate::{ - aws_ops::ebs::create_ebs_client, - k8s_ops::volume_snapshots::{ - construct_volume_snapshot_resource, wait_untill_snapshot_is_ready, - }, -}; - -/// A struct for holding the Kubernetes APIs for the backup operation -struct SourceKubernetesApisStruct { - source_vs_api: Api, - vsc_api: Api, -} - -/// A struct for backing up a PVC to a VolumeSnapshot -pub struct BackupOperator { - region: String, - source_ns: String, - volume_snapshot_class: String, - pvc_name: String, - volume_snapshot_name: String, -} - -impl BackupOperator { - /// Creates a new BackupOperator instance - /// - /// # Arguments - /// - /// * `region` - Region where the EBS volumes are stored - /// * `source_ns` - Source namespace - /// * `volume_snapshot_class` - VolumeSnapshotClass name - /// * `pvc_name` - PVC name - /// * `volume_snapshot_name` - VolumeSnapshot name +#[async_trait] +/// This trait represents the BackupOperator +pub trait BackupOperator { + /// Backs up a PVC to a VolumeSnapshot /// /// # Returns /// - /// BackupOperator instance + /// Result /// /// # Example /// /// ``` - /// use crate::backup::backup_operator::BackupOperator; - /// let backup_operator = BackupOperator::new( + /// use crate::backup::backup_operator::BackupOperatorImpl; + /// let backup_operator = BackupOperatorImpl::new( /// "eu-west-1".to_string(), /// "source-ns".to_string(), /// "volume-snapshot-class".to_string(), /// "pvc-name".to_string(), + /// false, /// "volume-snapshot-name".to_string(), /// ); + /// backup_operator.backup().await?; /// ``` + async fn backup(&self) -> Result<()>; +} + +impl BackupOperatorImpl { pub fn new( region: String, source_ns: String, volume_snapshot_class: String, pvc_name: String, + include_all: bool, volume_snapshot_name: String, ) -> Self { - BackupOperator { + Self { region, source_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, } } +} - /// Backs up a PVC to a VolumeSnapshot - /// - /// # Returns - /// - /// Result - /// - /// # Example - /// - /// ``` - /// use crate::backup::backup_operator::BackupOperator; - /// let backup_operator = BackupOperator::new( - /// "eu-west-1".to_string(), - /// "source-ns".to_string(), - /// "volume-snapshot-class".to_string(), - /// "pvc-name".to_string(), - /// "volume-snapshot-name".to_string(), - /// ); - /// backup_operator.backup().await?; - /// ``` - pub async fn backup(&self) -> Result<()> { +#[async_trait] +impl BackupOperator for BackupOperatorImpl { + async fn backup(&self) -> Result<()> { // Create a Kubernetes client let k8s_client = Client::try_default().await?; @@ -100,55 +73,83 @@ impl BackupOperator { // Define the VolumeSnapshot and VolumeSnapshotContent APIs let source_kubernetes_apis_struct = SourceKubernetesApisStruct { source_vs_api: Api::namespaced(k8s_client.clone(), &self.source_ns), + source_pvcs_api: Api::namespaced(k8s_client.clone(), &self.source_ns), vsc_api: Api::all(k8s_client.clone()), }; - // Create a VolumeSnapshot resource on Source Namespace - // let vs_creator_source_ns = VolumeSnapshotCreator::new( - // ... - // ); - // vs_creator_source_ns.create().await?; - let volume_snapshot = construct_volume_snapshot_resource( - self.volume_snapshot_name.to_string(), - self.source_ns.to_string(), - self.volume_snapshot_class.to_string(), - Some(self.pvc_name.to_string()), - None, - None, - None, - ); + // Check if we will backup all PVCs in the namespace + if self.include_all && self.pvc_name.is_empty() { + let pvcs = get_pvcs_available(&source_kubernetes_apis_struct.source_pvcs_api).await?; + info!("Available PVCs: {:?}", pvcs); + bail!("Include all PVCs is not implemented yet"); + } else { + // We will backup a single PVC + // Check if the PVC exists + check_if_pvc_exists( + &source_kubernetes_apis_struct.source_pvcs_api, + &self.pvc_name, + ) + .await?; - let pp = PostParams::default(); - let status: VolumeSnapshotStatus = match source_kubernetes_apis_struct - .source_vs_api - .create(&pp, &volume_snapshot) - .await - { - Ok(snapshot) => { - info!("Created VolumeSnapshot: {:?}", snapshot); - wait_untill_snapshot_is_ready( - source_kubernetes_apis_struct.source_vs_api, - source_kubernetes_apis_struct.vsc_api, - ebs_client.await.unwrap(), - &self.volume_snapshot_name, - ) - .await? - } - Err(e) => { - bail!("Failed to create VolumeSnapshot: {}", e); - } - }; + let vs_operator = VolumeSnapshotOperator::new( + self.volume_snapshot_name.to_string(), + self.source_ns.to_string(), + self.volume_snapshot_class.to_string(), + Some(self.pvc_name.to_string()), + None, + ); - let bound_vsc_name = status.bound_volume_snapshot_content_name.unwrap(); - let restore_size = status.restore_size.unwrap(); - info!( - "{}", - format!( - "VolumeSnapshot is ready! Bound vsc name: {}, restore_size: {}", - bound_vsc_name, restore_size - ) - ); + let volume_snapshot = vs_operator.construct_volume_snapshot_resource(None, None); + + let pp = PostParams::default(); + let status: VolumeSnapshotStatus = match source_kubernetes_apis_struct + .source_vs_api + .create(&pp, &volume_snapshot) + .await + { + Ok(snapshot) => { + info!("Created VolumeSnapshot: {:?}", snapshot); + wait_untill_snapshot_is_ready( + source_kubernetes_apis_struct.source_vs_api, + source_kubernetes_apis_struct.vsc_api, + ebs_client.await.unwrap(), + &self.volume_snapshot_name, + ) + .await? + } + Err(e) => { + bail!("Failed to create VolumeSnapshot: {}", e); + } + }; + + let bound_vsc_name = status.bound_volume_snapshot_content_name.unwrap(); + let restore_size = status.restore_size.unwrap(); + info!( + "{}", + format!( + "VolumeSnapshot is ready! Bound vsc name: {}, restore_size: {}", + bound_vsc_name, restore_size + ) + ); + } Ok(()) } } + +/// A struct for holding the Kubernetes APIs for the backup operation +struct SourceKubernetesApisStruct { + source_vs_api: Api, + source_pvcs_api: Api, + vsc_api: Api, +} + +/// A struct for backing up a PVC to a VolumeSnapshot +pub struct BackupOperatorImpl { + region: String, + source_ns: String, + volume_snapshot_class: String, + pvc_name: String, + include_all: bool, + volume_snapshot_name: String, +} diff --git a/src/k8s_ops/mod.rs b/src/k8s_ops/mod.rs index 0b737aa..3abea93 100644 --- a/src/k8s_ops/mod.rs +++ b/src/k8s_ops/mod.rs @@ -1,3 +1,4 @@ pub mod persistent_volume_claims; +pub mod persistent_volume_claims_payload; pub mod volume_snapshot_contents; pub mod volume_snapshots; diff --git a/src/k8s_ops/persistent_volume_claims.rs b/src/k8s_ops/persistent_volume_claims.rs index a8ca333..c92f19f 100644 --- a/src/k8s_ops/persistent_volume_claims.rs +++ b/src/k8s_ops/persistent_volume_claims.rs @@ -1,3 +1,5 @@ +use super::persistent_volume_claims_payload::PVCOperatorPayload; +use anyhow::Result; use k8s_openapi::{ api::core::v1::{ PersistentVolumeClaim, PersistentVolumeClaimSpec, TypedLocalObjectReference, @@ -5,73 +7,124 @@ use k8s_openapi::{ }, apimachinery::pkg::api::resource::Quantity, }; -use kube::api::ObjectMeta; +use kube::api::{ListParams, ObjectMeta}; use std::collections::BTreeMap; +use tracing::info; -/// Construct a PersistentVolumeClaim resource -/// -/// # Arguments -/// -/// * `pvc_name` - Name of the PersistentVolumeClaim resource -/// * `namespace` - Namespace of the PersistentVolumeClaim resource -/// * `storage_class` - Name of the StorageClass resource -/// * `access_modes` - Access modes for the PersistentVolumeClaim resource -/// * `volume_snapshot_name` - Name of the VolumeSnapshot resource -/// * `restore_size` - Size of the PersistentVolumeClaim resource -/// -/// # Returns -/// -/// PersistentVolumeClaim resource -pub fn construct_persistent_volume_claim_resource( - pvc_name: String, - namespace: String, - storage_class: Option, - access_modes: Option>, - volume_snapshot_name: String, - restore_size: String, -) -> PersistentVolumeClaim { - // Create a base labels map - let mut labels = BTreeMap::new(); +pub struct PVCOperator { + pvc_operator_payload: PVCOperatorPayload, +} - // Always add the VSc name - labels.insert( - "pvc-snapshotter/volume-snapshot-name".to_string(), - volume_snapshot_name.to_string(), - ); +impl PVCOperator { + pub fn new(pvc_operator_payload: PVCOperatorPayload) -> Self { + Self { + pvc_operator_payload, + } + } - PersistentVolumeClaim { - metadata: ObjectMeta { - name: Some(pvc_name), - namespace: Some(namespace.clone()), - labels: Some(labels), - ..Default::default() - }, - spec: Some(PersistentVolumeClaimSpec { - access_modes: Some(access_modes.unwrap_or(vec!["ReadWriteOnce".to_string()])), - storage_class_name: Some(storage_class.unwrap_or("gp3".to_string())), - data_source: Some(TypedLocalObjectReference { - name: volume_snapshot_name.to_string(), - kind: "VolumeSnapshot".to_string(), - api_group: Some("snapshot.storage.k8s.io".to_string()), - }), - data_source_ref: Some(TypedObjectReference { - name: volume_snapshot_name.to_string(), - kind: "VolumeSnapshot".to_string(), - api_group: Some("snapshot.storage.k8s.io".to_string()), - namespace: Some(namespace), - }), - volume_mode: Some("Filesystem".to_string()), - volume_name: Default::default(), - resources: Some(VolumeResourceRequirements { - requests: Some(BTreeMap::from([( - "storage".to_string(), - Quantity(restore_size.to_string()), - )])), + /// Construct a PersistentVolumeClaim resource + /// + /// # Arguments + /// + /// * `pvc_name` - Name of the PersistentVolumeClaim resource + /// * `namespace` - Namespace of the PersistentVolumeClaim resource + /// * `storage_class` - Name of the StorageClass resource + /// * `access_modes` - Access modes for the PersistentVolumeClaim resource + /// * `volume_snapshot_name` - Name of the VolumeSnapshot resource + /// * `restore_size` - Size of the PersistentVolumeClaim resource + /// + /// # Returns + /// + /// PersistentVolumeClaim resource + pub fn construct_persistent_volume_claim_resource(&self) -> PersistentVolumeClaim { + // Create a base labels map + let mut labels = BTreeMap::new(); + + // Always add the VSc name + labels.insert( + "pvc-snapshotter/volume-snapshot-name".to_string(), + self.pvc_operator_payload.pvc_name().to_string(), + ); + + PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some(String::from(self.pvc_operator_payload.pvc_name())), + namespace: Some(String::from(self.pvc_operator_payload.namespace())), + labels: Some(labels), ..Default::default() + }, + spec: Some(PersistentVolumeClaimSpec { + access_modes: Some( + self.pvc_operator_payload + .access_modes() + .unwrap_or(vec!["ReadWriteOnce".to_string()]), + ), + storage_class_name: Some( + self.pvc_operator_payload + .storage_class() + .unwrap_or("gp3") + .to_string(), + ), + data_source: Some(TypedLocalObjectReference { + name: String::from(self.pvc_operator_payload.volume_snapshot_name()), + kind: "VolumeSnapshot".to_string(), + api_group: Some("snapshot.storage.k8s.io".to_string()), + }), + data_source_ref: Some(TypedObjectReference { + name: String::from(self.pvc_operator_payload.volume_snapshot_name()), + kind: "VolumeSnapshot".to_string(), + api_group: Some("snapshot.storage.k8s.io".to_string()), + namespace: Some(String::from(self.pvc_operator_payload.namespace())), + }), + volume_mode: Some("Filesystem".to_string()), + volume_name: Default::default(), + resources: Some(VolumeResourceRequirements { + requests: Some(BTreeMap::from([( + "storage".to_string(), + Quantity(String::from(self.pvc_operator_payload.restore_size())), + )])), + ..Default::default() + }), + selector: Default::default(), + volume_attributes_class_name: Default::default(), }), - selector: Default::default(), - volume_attributes_class_name: Default::default(), - }), - ..Default::default() + ..Default::default() + } + } +} + +/// Get the list of PersistentVolumeClaims available +pub async fn get_pvcs_available( + target_pvc_api: &kube::Api, +) -> Result> { + let lp = ListParams::default(); + let pvc_list: Vec<_> = match target_pvc_api.list(&lp).await { + Ok(pvc) => pvc, + Err(e) => panic!("Failed to list PVCs: {}", e), + } + .into_iter() + .map(|pvc| pvc.metadata.name.unwrap()) + .collect(); + + Ok(pvc_list) +} + +pub async fn check_if_pvc_exists( + target_pvc_api: &kube::Api, + pvc_name: &str, +) -> Result { + match target_pvc_api.get(pvc_name).await { + Ok(pvc) => { + info!( + "{}", + format!( + "PVC exists: {} on target namespace {:?}", + pvc.metadata.name.clone().unwrap(), + pvc.metadata.namespace.clone().unwrap() + ) + ); + Ok(pvc) + } + Err(e) => panic!("Failed to get PVC: {}", e), } } diff --git a/src/k8s_ops/persistent_volume_claims_payload.rs b/src/k8s_ops/persistent_volume_claims_payload.rs new file mode 100644 index 0000000..e50c909 --- /dev/null +++ b/src/k8s_ops/persistent_volume_claims_payload.rs @@ -0,0 +1,66 @@ +pub struct PVCOperatorPayload { + pub pvc_name: String, + pub namespace: String, + pub storage_class: Option, + pub access_modes: Option>, + pub volume_snapshot_name: String, + pub restore_size: String, +} + +impl PVCOperatorPayload { + /// Creates a new PVCOperatorPayload + /// + /// # Arguments + /// + /// * `pvc_name` - Name of the PersistentVolumeClaim resource + /// * `namespace` - Namespace of the PersistentVolumeClaim resource + /// * `storage_class` - Name of the StorageClass resource + /// * `access_modes` - Access modes for the PersistentVolumeClaim resource + /// * `volume_snapshot_name` - Name of the VolumeSnapshot resource + /// * `restore_size` - Size of the PersistentVolumeClaim resource + /// + /// # Returns + /// + /// A new PVCOperatorPayload instance + pub fn new( + pvc_name: impl Into, + namespace: impl Into, + storage_class: impl Into>, + access_modes: impl Into>>, + volume_snapshot_name: impl Into, + restore_size: impl Into, + ) -> Self { + Self { + pvc_name: pvc_name.into(), + namespace: namespace.into(), + storage_class: storage_class.into(), + access_modes: access_modes.into(), + volume_snapshot_name: volume_snapshot_name.into(), + restore_size: restore_size.into(), + } + } + + pub fn pvc_name(&self) -> &str { + &self.pvc_name + } + + pub fn namespace(&self) -> &str { + &self.namespace + } + + pub fn storage_class(&self) -> Option<&str> { + self.storage_class.as_deref() + } + + pub fn access_modes(&self) -> Option> { + self.access_modes.clone() + } + + pub fn volume_snapshot_name(&self) -> &str { + &self.volume_snapshot_name + } + + pub fn restore_size(&self) -> &str { + &self.restore_size + } +} diff --git a/src/k8s_ops/volume_snapshot_contents.rs b/src/k8s_ops/volume_snapshot_contents.rs index be243c3..06a80cf 100644 --- a/src/k8s_ops/volume_snapshot_contents.rs +++ b/src/k8s_ops/volume_snapshot_contents.rs @@ -5,59 +5,79 @@ use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontent VolumeSnapshotContentSpec, VolumeSnapshotContentStatus, VolumeSnapshotContentVolumeSnapshotRef, }; -/// Construct a VolumeSnapshotContent resource -/// -/// # Arguments -/// -/// * `name` - Name of the VolumeSnapshotContent resource -/// * `namespace` - Namespace of the VolumeSnapshotContent resource -/// * `volume_snapshot_name` - Name of the VolumeSnapshot resource -/// * `volume_snapshot_class` - Name of the VolumeSnapshotClass resource -/// * `source_volume_handle` - Handle - Snapshot ID of the source volume -/// -/// # Returns -/// -/// VolumeSnapshotContent resource -pub fn construct_volume_snapshot_content_resource( - name: String, - namespace: String, - volume_snapshot_name: String, - volume_snapshot_class: Option, - source_volume_handle: Option, -) -> VolumeSnapshotContent { - VolumeSnapshotContent { - metadata: ObjectMeta { - name: Some(name), - namespace: Some(namespace.clone()), - ..Default::default() - }, - spec: VolumeSnapshotContentSpec { - volume_snapshot_ref: VolumeSnapshotContentVolumeSnapshotRef { - api_version: Some("snapshot.storage.k8s.io/v1".to_string()), - kind: Some("VolumeSnapshot".to_string()), - name: Some(volume_snapshot_name), - namespace: Some(namespace), - field_path: Default::default(), - resource_version: Default::default(), - uid: Default::default(), - }, - deletion_policy: VolumeSnapshotContentDeletionPolicy::Retain, - driver: "ebs.csi.aws.com".to_string(), - source: VolumeSnapshotContentSource { - snapshot_handle: source_volume_handle.clone(), +pub struct VolumeSnapshotContentOperator { + pub name: String, + pub namespace: String, + pub volume_snapshot_name: String, + pub volume_snapshot_class: Option, + pub source_volume_handle: Option, +} + +impl VolumeSnapshotContentOperator { + pub fn new( + name: String, + namespace: String, + volume_snapshot_name: String, + volume_snapshot_class: Option, + source_volume_handle: Option, + ) -> Self { + Self { + name, + namespace, + volume_snapshot_name, + volume_snapshot_class, + source_volume_handle, + } + } + + /// Construct a VolumeSnapshotContent resource + /// + /// # Arguments + /// + /// * `name` - Name of the VolumeSnapshotContent resource + /// * `namespace` - Namespace of the VolumeSnapshotContent resource + /// * `volume_snapshot_name` - Name of the VolumeSnapshot resource + /// * `volume_snapshot_class` - Name of the VolumeSnapshotClass resource + /// * `source_volume_handle` - Handle - Snapshot ID of the source volume + /// + /// # Returns + /// + /// VolumeSnapshotContent resource + pub fn construct_volume_snapshot_content_resource(&self) -> VolumeSnapshotContent { + VolumeSnapshotContent { + metadata: ObjectMeta { + name: Some(self.name.clone()), + namespace: Some(self.namespace.clone()), ..Default::default() }, - volume_snapshot_class_name: volume_snapshot_class, - source_volume_mode: Some("Filesystem".to_string()), - }, - status: Some(VolumeSnapshotContentStatus { - snapshot_handle: source_volume_handle, - creation_time: Default::default(), - ready_to_use: Default::default(), - restore_size: Default::default(), - error: Default::default(), - volume_group_snapshot_handle: Default::default(), - }), + spec: VolumeSnapshotContentSpec { + volume_snapshot_ref: VolumeSnapshotContentVolumeSnapshotRef { + api_version: Some("snapshot.storage.k8s.io/v1".to_string()), + kind: Some("VolumeSnapshot".to_string()), + name: Some(self.volume_snapshot_name.clone()), + namespace: Some(self.namespace.clone()), + field_path: Default::default(), + resource_version: Default::default(), + uid: Default::default(), + }, + deletion_policy: VolumeSnapshotContentDeletionPolicy::Retain, + driver: "ebs.csi.aws.com".to_string(), + source: VolumeSnapshotContentSource { + snapshot_handle: self.source_volume_handle.clone(), + ..Default::default() + }, + volume_snapshot_class_name: self.volume_snapshot_class.clone(), + source_volume_mode: Some("Filesystem".to_string()), + }, + status: Some(VolumeSnapshotContentStatus { + snapshot_handle: self.source_volume_handle.clone(), + creation_time: Default::default(), + ready_to_use: Default::default(), + restore_size: Default::default(), + error: Default::default(), + volume_group_snapshot_handle: Default::default(), + }), + } } } diff --git a/src/k8s_ops/volume_snapshots.rs b/src/k8s_ops/volume_snapshots.rs index b68af6c..7b688da 100644 --- a/src/k8s_ops/volume_snapshots.rs +++ b/src/k8s_ops/volume_snapshots.rs @@ -10,82 +10,109 @@ use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshots::{ use std::collections::BTreeMap; use std::time::Duration; use tokio::time::sleep; -use tracing::{error, info}; +use tracing::{info, warn}; -/// Construct a VolumeSnapshot resource -/// -/// # Arguments -/// -/// * `name` - Name of the VolumeSnapshot resource -/// * `namespace` - Namespace of the VolumeSnapshot resource -/// * `volume_snapshot_class` - Name of the VolumeSnapshotClass resource -/// * `source_pvc_name` - Name of the PersistentVolumeClaim resource -/// * `vsc_name` - Name of the VolumeSnapshotContent resource -/// -/// # Returns -/// -/// VolumeSnapshot resource -pub fn construct_volume_snapshot_resource( - name: String, - namespace: String, - volume_snapshot_class: String, - source_pvc_name: Option, - vsc_name: Option, - snapshot_handle: Option, - restore_size: Option, -) -> VolumeSnapshot { - // Create a base annotations map - let mut annotations = BTreeMap::new(); - // Always add the CSI driver annotation - annotations.insert( - "pvc-snapshotter/csi-driver-name".to_string(), - "ebs.csi.aws.com".to_string(), - ); - // Always add the VSC deletion policy annotation - annotations.insert( - "pvc-snapshotter/csi-vsc-deletion-policy".to_string(), - "Retain".to_string(), - ); +pub struct VolumeSnapshotOperator { + pub name: String, + pub namespace: String, + pub volume_snapshot_class: String, + pub source_pvc_name: Option, + pub vsc_name: Option, +} + +impl VolumeSnapshotOperator { + pub fn new( + name: String, + namespace: String, + volume_snapshot_class: String, + source_pvc_name: Option, + vsc_name: Option, + ) -> Self { + Self { + name, + namespace, + volume_snapshot_class, + source_pvc_name, + vsc_name, + } + } - // If a snapshot handle is provided, insert the corresponding annotation - if let Some(handle) = snapshot_handle { + /// Construct a VolumeSnapshot resource + /// + /// # Arguments + /// + /// * `name` - Name of the VolumeSnapshot resource + /// * `namespace` - Namespace of the VolumeSnapshot resource + /// * `volume_snapshot_class` - Name of the VolumeSnapshotClass resource + /// * `source_pvc_name` - Name of the PersistentVolumeClaim resource + /// * `vsc_name` - Name of the VolumeSnapshotContent resource + /// * `snapshot_handle` - Handle - Snapshot ID of the source volume + /// * `restore_size` - Size of the restored volume + /// + /// # Returns + /// + /// VolumeSnapshot resource + pub fn construct_volume_snapshot_resource( + &self, + snapshot_handle: Option, + restore_size: Option, + ) -> VolumeSnapshot { + // Create a base annotations map + let mut annotations = BTreeMap::new(); + // Always add the CSI driver annotation annotations.insert( - "pvc-snapshotter/csi-volumesnapshot-handle".to_string(), - handle, + "pvc-snapshotter/csi-driver-name".to_string(), + "ebs.csi.aws.com".to_string(), ); - } - // If a restore size is provided, insert the corresponding annotation - if let Some(size) = restore_size { + // Always add the VSC deletion policy annotation annotations.insert( - "pvc-snapshotter/csi-volumesnapshot-restore-size".to_string(), - size, + "pvc-snapshotter/csi-vsc-deletion-policy".to_string(), + "Retain".to_string(), ); - } - // Create a base labels map - let mut labels = BTreeMap::new(); - // Always add the namespace name - labels.insert("app.kubernetes.io/instance".to_string(), namespace.clone()); + // If a snapshot handle is provided, insert the corresponding annotation + if let Some(handle) = snapshot_handle { + annotations.insert( + "pvc-snapshotter/csi-volumesnapshot-handle".to_string(), + handle, + ); + } + // If a restore size is provided, insert the corresponding annotation + if let Some(size) = restore_size { + annotations.insert( + "pvc-snapshotter/csi-volumesnapshot-restore-size".to_string(), + size, + ); + } - VolumeSnapshot { - metadata: ObjectMeta { - name: Some(name), - namespace: Some(namespace), - annotations: Some(annotations), - finalizers: Some(vec![ - "snapshot.storage.kubernetes.io/volumesnapshot-bound-protection".to_string(), - ]), - labels: Some(labels), - ..Default::default() - }, - spec: VolumeSnapshotSpec { - volume_snapshot_class_name: Some(volume_snapshot_class), - source: VolumeSnapshotSource { - persistent_volume_claim_name: source_pvc_name, - volume_snapshot_content_name: vsc_name, + // Create a base labels map + let mut labels = BTreeMap::new(); + // Always add the namespace name + labels.insert( + "app.kubernetes.io/instance".to_string(), + self.namespace.clone(), + ); + + VolumeSnapshot { + metadata: ObjectMeta { + name: Some(self.name.clone()), + namespace: Some(self.namespace.clone()), + annotations: Some(annotations), + finalizers: Some(vec![ + "snapshot.storage.kubernetes.io/volumesnapshot-bound-protection".to_string(), + ]), + labels: Some(labels), + ..Default::default() + }, + spec: VolumeSnapshotSpec { + volume_snapshot_class_name: Some(self.volume_snapshot_class.clone()), + source: VolumeSnapshotSource { + persistent_volume_claim_name: self.source_pvc_name.clone(), + volume_snapshot_content_name: self.vsc_name.clone(), + }, }, - }, - ..Default::default() + ..Default::default() + } } } @@ -94,6 +121,8 @@ pub fn construct_volume_snapshot_resource( /// # Arguments /// /// * `vs_api` - Api object for VolumeSnapshot +/// * `vsc_api` - Api object for VolumeSnapshotContent +/// * `ebs_client` - EBS Client object /// * `volume_snapshot_name` - Name of the VolumeSnapshot resource /// /// # Returns @@ -128,7 +157,7 @@ pub async fn wait_untill_snapshot_is_ready( ); } Err(e) => { - error!("Failed to get snapshot handle: {}", e); + warn!("Failed to get snapshot handle: {}", e); } } sleep(Duration::from_secs(5)).await; diff --git a/src/main.rs b/src/main.rs index 1ee74b0..ef56bee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,10 @@ mod backup; mod k8s_ops; mod restore; use anyhow::Result; -use backup::backup_operator::BackupOperator; +use backup::backup_operator::{BackupOperator, BackupOperatorImpl}; use clap::{Parser, Subcommand}; use colored::Colorize; -use restore::restore_operator::RestoreOperator; +use restore::restore_operator::{RestoreOperator, RestoreOperatorImpl}; use tracing::info; #[derive(Parser)] @@ -30,8 +30,16 @@ enum Commands { #[arg(long, required = true)] volume_snapshot_class: String, /// PVC name - #[arg(long, required = true)] + #[arg(long, required = false, conflicts_with = "include_all")] pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "true", + conflicts_with = "pvc_name" + )] + include_all: bool, /// VolumeSnapshot name #[arg(long, required = true)] volume_snapshot_name: String, @@ -47,8 +55,16 @@ enum Commands { #[arg(long, required = true)] volume_snapshot_class: String, /// PVC name - #[arg(long, required = true)] + #[arg(long, required = false, conflicts_with = "include_all")] pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "true", + conflicts_with = "pvc_name" + )] + include_all: bool, /// VolumeSnapshot name #[arg(long, required = true)] volume_snapshot_name: String, @@ -73,8 +89,16 @@ enum Commands { #[arg(long, required = true)] volume_snapshot_class: String, /// PVC name - #[arg(long, required = true)] + #[arg(long, required = false, conflicts_with = "include_all")] pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "true", + conflicts_with = "pvc_name" + )] + include_all: bool, /// VolumeSnapshot name #[arg(long, required = true)] volume_snapshot_name: String, @@ -98,38 +122,50 @@ async fn main() -> Result<()> { source_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, } => { - let backup_operator = BackupOperator::new( + let backup_operator = BackupOperatorImpl::new( region, source_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, ); info!("{}", "Starting Backup process...".bold().blue()); backup_operator.backup().await?; + info!( + "{}", + "Backup process completed successfully!".bold().green() + ); } Commands::Restore { source_ns, target_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, target_snapshot_content_name, storage_class_name, } => { - let restore_operator = RestoreOperator::new( + let restore_operator = RestoreOperatorImpl::new( source_ns, target_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, target_snapshot_content_name, storage_class_name, ); info!("{}", "Starting Restore process...".bold().blue()); restore_operator.restore().await?; + info!( + "{}", + "Restore process completed successfully!".bold().green() + ); } Commands::Full { region, @@ -137,30 +173,41 @@ async fn main() -> Result<()> { target_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, target_snapshot_content_name, storage_class_name, } => { - let backup_operator = BackupOperator::new( + let backup_operator = BackupOperatorImpl::new( region.clone(), source_ns.clone(), volume_snapshot_class.clone(), pvc_name.clone(), + include_all, volume_snapshot_name.clone(), ); - let restore_operator = RestoreOperator::new( + let restore_operator = RestoreOperatorImpl::new( source_ns, target_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, target_snapshot_content_name, storage_class_name, ); info!("{}", "Starting Backup process...".bold().blue()); backup_operator.backup().await?; + info!( + "{}", + "Backup process completed successfully!".bold().green() + ); info!("{}", "Starting Restore process...".bold().blue()); restore_operator.restore().await?; + info!( + "{}", + "Restore process completed successfully!".bold().green() + ); } }; Ok(()) diff --git a/src/restore/restore_operator.rs b/src/restore/restore_operator.rs index 7db78fd..b7112bc 100644 --- a/src/restore/restore_operator.rs +++ b/src/restore/restore_operator.rs @@ -1,9 +1,11 @@ use crate::k8s_ops::{ - persistent_volume_claims::construct_persistent_volume_claim_resource, - volume_snapshot_contents::{construct_volume_snapshot_content_resource, get_snapshot_handle}, - volume_snapshots::construct_volume_snapshot_resource, + persistent_volume_claims::PVCOperator, + persistent_volume_claims_payload::PVCOperatorPayload, + volume_snapshot_contents::{get_snapshot_handle, VolumeSnapshotContentOperator}, + volume_snapshots::VolumeSnapshotOperator, }; use anyhow::{bail, Result}; +use async_trait::async_trait; use k8s_openapi::api::core::v1::PersistentVolumeClaim; use kube::{api::PostParams, Api, Client}; use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::{ @@ -12,105 +14,69 @@ use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::{ }; use tracing::info; -/// A struct for holding the Kubernetes APIs for the restore operation -struct TargetKubernetesApisStruct { - source_vs_api: Api, - vsc_api: Api, - pvcs_api: Api, -} - -/// A struct for restoring a PVC from a VolumeSnapshot -pub struct RestoreOperator { - source_ns: String, - target_ns: String, - volume_snapshot_class: String, - pvc_name: String, - volume_snapshot_name: String, - target_snapshot_content_name: String, - storage_class_name: String, -} - -impl RestoreOperator { - /// Creates a new RestoreOperator instance - /// - /// # Arguments - /// - /// * `source_ns` - Source namespace - /// * `target_ns` - Target namespace - /// * `volume_snapshot_class` - VolumeSnapshotClass name - /// * `pvc_name` - PVC name - /// * `volume_snapshot_name` - VolumeSnapshot name - /// * `target_snapshot_content_name` - Target VolumeSnapshotContent name - /// * `storage_class_name` - StorageClass name +#[async_trait] +pub trait RestoreOperator { + /// Restores a PVC from a VolumeSnapshot /// /// # Returns /// - /// RestoreOperator instance + /// Result /// /// # Example /// /// ``` - /// use crate::restore::restore_operator::RestoreOperator; - /// let restore_operator = RestoreOperator::new( + /// use crate::restore::restore_operator::RestoreOperatorImpl; + /// let restore_operator = RestoreOperatorImpl::new( /// "source-ns".to_string(), /// "target-ns".to_string(), /// "volume-snapshot-class".to_string(), /// "pvc-name".to_string(), + /// false, /// "volume-snapshot-name".to_string(), /// "target-snapshot-content-name".to_string(), /// "storage-class-name".to_string(), /// ); + /// restore_operator.restore().await?; /// ``` + async fn restore(&self) -> Result<()>; +} + +impl RestoreOperatorImpl { pub fn new( source_ns: String, target_ns: String, volume_snapshot_class: String, pvc_name: String, + include_all: bool, volume_snapshot_name: String, target_snapshot_content_name: String, storage_class_name: String, ) -> Self { - RestoreOperator { + Self { source_ns, target_ns, volume_snapshot_class, pvc_name, + include_all, volume_snapshot_name, target_snapshot_content_name, storage_class_name, } } +} - /// Restores a PVC from a VolumeSnapshot - /// - /// # Returns - /// - /// Result - /// - /// # Example - /// - /// ``` - /// use crate::restore::restore_operator::RestoreOperator; - /// let restore_operator = RestoreOperator::new( - /// "source-ns".to_string(), - /// "target-ns".to_string(), - /// "volume-snapshot-class".to_string(), - /// "pvc-name".to_string(), - /// "volume-snapshot-name".to_string(), - /// "target-snapshot-content-name".to_string(), - /// "storage-class-name".to_string(), - /// ); - /// restore_operator.restore().await?; - /// ``` - pub async fn restore(&self) -> Result<()> { +#[async_trait] +impl RestoreOperator for RestoreOperatorImpl { + async fn restore(&self) -> Result<()> { // Create a Kubernetes client let k8s_client = Client::try_default().await?; // Define the VolumeSnapshot, VolumeSnapshotContent and PersistentVolumeClaim APIs let target_kubernetes_apis_struct = TargetKubernetesApisStruct { source_vs_api: Api::namespaced(k8s_client.clone(), &self.source_ns), + target_pvcs_api: Api::namespaced(k8s_client.clone(), &self.target_ns), + target_vs_api: Api::namespaced(k8s_client.clone(), &self.target_ns), vsc_api: Api::all(k8s_client.clone()), - pvcs_api: Api::namespaced(k8s_client.clone(), &self.target_ns), }; let status: VolumeSnapshotStatus = match target_kubernetes_apis_struct @@ -133,19 +99,16 @@ impl RestoreOperator { ) .await?; - // Create a VolumeSnapshotContent resource on Target Namespace - // let vs_creator_target_ns = VolumeSnapshotContentCreator::new( - // ... - // ); - // vs_creator_target_ns.create().await?; - let snapshot_content = construct_volume_snapshot_content_resource( - self.target_snapshot_content_name.to_string(), - self.target_ns.to_string(), - self.volume_snapshot_name.to_string(), - Some(self.volume_snapshot_class.to_string()), - Some(snapshot_handle.to_string()), + let vsc_operator = VolumeSnapshotContentOperator::new( + self.target_snapshot_content_name.clone(), + self.target_ns.clone(), + self.volume_snapshot_name.clone(), + Some(self.volume_snapshot_class.clone()), + Some(snapshot_handle.clone()), ); + let snapshot_content = vsc_operator.construct_volume_snapshot_content_resource(); + let pp = PostParams::default(); match target_kubernetes_apis_struct .vsc_api @@ -156,49 +119,49 @@ impl RestoreOperator { Err(e) => panic!("Failed to create VolumeSnapshotContent: {}", e), } - // Create a VolumeSnapshot resource in the Target Namespace - // let vs_creator = VolumeSnapshotCreator::new( - // ... - // ); - // vs_creator.create().await?; - let vs_api_target_ns: Api = Api::namespaced(k8s_client, &self.target_ns); - let target_volume_snapshot = construct_volume_snapshot_resource( - self.volume_snapshot_name.to_string(), - self.target_ns.to_string(), - self.volume_snapshot_class.to_string(), + let vs_operator = VolumeSnapshotOperator::new( + self.volume_snapshot_name.clone(), + self.target_ns.clone(), + self.volume_snapshot_class.clone(), None, Some(self.target_snapshot_content_name.to_string()), + ); + + let target_volume_snapshot = vs_operator.construct_volume_snapshot_resource( Some(snapshot_handle.to_string()), Some(restore_size.to_string()), ); info!("Creating VolumeSnapshot in the target namespace..."); let pp = PostParams::default(); - match vs_api_target_ns.create(&pp, &target_volume_snapshot).await { + match target_kubernetes_apis_struct + .target_vs_api + .create(&pp, &target_volume_snapshot) + .await + { Ok(target_volume_snapshot) => { info!("Created VolumeSnapshot: {:?}", target_volume_snapshot) } Err(e) => panic!("Failed to create VolumeSnapshot: {}", e), } - // Create the PVC from the VolumeSnapshot to Target Namespace - // let pvc_creator = PersistentVolumeClaimCreator::new( - // ... - // ); - // pvc_creator.create().await?; - let pvc: PersistentVolumeClaim = construct_persistent_volume_claim_resource( - self.pvc_name.to_string(), - self.target_ns.to_string(), - Some(self.storage_class_name.to_string()), + // Restore the PVC for each pvc available + let pvc_payload = PVCOperatorPayload::new( + self.pvc_name.clone(), + self.target_ns.clone(), + Some(self.storage_class_name.clone()), None, - self.volume_snapshot_name.to_string(), - restore_size.to_string(), + self.volume_snapshot_name.clone(), + restore_size, ); + let pvc_operator = PVCOperator::new(pvc_payload); + let pvc = pvc_operator.construct_persistent_volume_claim_resource(); + info!("Restoring PVC..."); let pp = PostParams::default(); match target_kubernetes_apis_struct - .pvcs_api + .target_pvcs_api .create(&pp, &pvc) .await { @@ -209,3 +172,23 @@ impl RestoreOperator { Ok(()) } } + +/// A struct for holding the Kubernetes APIs for the restore operation +struct TargetKubernetesApisStruct { + source_vs_api: Api, + target_pvcs_api: Api, + target_vs_api: Api, + vsc_api: Api, +} + +/// A struct for restoring a PVC from a VolumeSnapshot +pub struct RestoreOperatorImpl { + source_ns: String, + target_ns: String, + volume_snapshot_class: String, + pvc_name: String, + include_all: bool, + volume_snapshot_name: String, + target_snapshot_content_name: String, + storage_class_name: String, +}