From 3cfc23d286750376015476bce848f81381aa7f4a Mon Sep 17 00:00:00 2001 From: nikoshet Date: Fri, 18 Oct 2024 15:47:45 +0300 Subject: [PATCH] Updates --- .github/workflows/ci.yaml | 16 +- .github/workflows/tests-workflow.yaml | 24 +++ Cargo.lock | 162 +++++++++++++----- Cargo.toml | 18 +- README.md | 32 ++-- .../Cargo.toml | 13 +- .../src/main.rs | 6 +- src/backup/backup_operator.rs | 15 +- src/k8s_ops/pvc/persistent_volume_claims.rs | 54 +++++- .../pvc/persistent_volume_claims_operator.rs | 42 +++-- .../pvc/persistent_volume_claims_tests.rs | 135 ++++++++++++--- src/k8s_ops/vs/volume_snapshots_operator.rs | 53 +++--- src/k8s_ops/vs/volume_snapshots_tests.rs | 8 +- .../vsc/volume_snapshot_contents_operator.rs | 27 ++- src/restore/restore_operator.rs | 22 ++- 15 files changed, 457 insertions(+), 170 deletions(-) create mode 100644 .github/workflows/tests-workflow.yaml rename {pvc-snapshotter-client => snap-kube-client}/Cargo.toml (66%) rename {pvc-snapshotter-client => snap-kube-client}/src/main.rs (97%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8571479..6720f34 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,14 +24,26 @@ jobs: format-and-clippy: uses: ./.github/workflows/format-workflow.yaml secrets: inherit + tests: + uses: ./.github/workflows/tests-workflow.yaml + secrets: inherit build: runs-on: ubuntu-latest - needs: [format-and-clippy] + needs: [format-and-clippy, tests] + strategy: + fail-fast: true + matrix: + include: + - name: "library" + path: "." + - name: "client" + path: "snap-kube-client" steps: - uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: rustfmt, clippy toolchain: ${{ env.RUST_VERSION }} - - name: Build + - name: Cargo Build ${{ matrix.name }} run: cargo build --verbose + working-directory: ${{ matrix.path }} diff --git a/.github/workflows/tests-workflow.yaml b/.github/workflows/tests-workflow.yaml new file mode 100644 index 0000000..82adc44 --- /dev/null +++ b/.github/workflows/tests-workflow.yaml @@ -0,0 +1,24 @@ +name: Tests Pipeline + +on: + workflow_call: + +env: + RUST_VERSION: 1.81.0 + +jobs: + tests: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt, clippy + toolchain: ${{ env.RUST_VERSION }} + - name: Install cargo-nextest + uses: baptiste0928/cargo-install@v3 + with: + crate: cargo-nextest + - name: Run tests + run: cargo nextest run --all diff --git a/Cargo.lock b/Cargo.lock index 8f3101e..c8610a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,6 +769,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -838,6 +844,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures" version = "0.3.30" @@ -1517,6 +1529,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1739,6 +1777,32 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1758,51 +1822,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pvc-snapshotter" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "aws-config", - "aws-sdk-ec2", - "clap", - "colored", - "k8s-openapi", - "kube", - "kube-custom-resources-rs", - "pretty_assertions", - "schemars", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "pvc-snapshotter-client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "aws-config", - "aws-sdk-ec2", - "clap", - "colored", - "k8s-openapi", - "kube", - "kube-custom-resources-rs", - "pretty_assertions", - "pvc-snapshotter", - "schemars", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "quote" version = "1.0.37" @@ -2247,6 +2266,53 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snap-kube" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-ec2", + "clap", + "colored", + "k8s-openapi", + "kube", + "kube-custom-resources-rs", + "mockall", + "pretty_assertions", + "schemars", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "snap-kube-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-ec2", + "clap", + "colored", + "k8s-openapi", + "kube", + "kube-custom-resources-rs", + "mockall", + "pretty_assertions", + "schemars", + "serde", + "serde_json", + "snap-kube", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "socket2" version = "0.5.7" @@ -2297,6 +2363,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.64" diff --git a/Cargo.toml b/Cargo.toml index 4abfc56..8f6e858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [package] -name = "pvc-snapshotter" +name = "snap-kube" version = "0.1.0" edition = "2021" license = "MIT" -description = "The pvc-snapshotter is a Rust tool that can backup and restore k8s PVCs using EBS snapshots" +description = "The snap-kube is a Rust tool that can backup and restore k8s PVCs using EBS snapshots" readme = "README.md" -homepage = "https://github.com/nikoshet/pvc-snapshotter" -repository = "https://github.com/nikoshet/pvc-snapshotter" +homepage = "https://github.com/nikoshet/snap-kube" +repository = "https://github.com/nikoshet/snap-kube" keywords = ["k8s", "pvc", "snapshot", "ebs", "backup", "restore", "aws"] -documentation = "https://docs.rs/pvc-snapshotter" +documentation = "https://docs.rs/snap-kube" exclude = ["script.sh"] [workspace] -members = ["pvc-snapshotter-client"] +members = ["snap-kube-client"] [workspace.dependencies] anyhow = "1.0.89" @@ -31,7 +31,8 @@ serde_json = "1.0.128" tokio = { version = "1", features = ["full"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" -pvc-snapshotter = { path = ".", version = "0.1" } +snap-kube = { path = ".", version = "0.1" } +mockall = "0.13" [dependencies] anyhow.workspace = true @@ -50,12 +51,13 @@ serde_json.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +mockall.workspace = true [lib] test = true edition = "2021" crate-type = ["lib"] -name = "pvc_snapshotter" +name = "snap_kube" [features] default = ["full"] diff --git a/README.md b/README.md index 05d1caa..98efd78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -

PVC-Snapshotter

+

SnapKube

- A Rust 🦀 tool supports PVC snapshots across Kubernetes namespaces (Velerust/Rustero) + A Rust 🦀 tool supports (for now) PVC snapshots across Kubernetes namespaces
@@ -9,23 +9,23 @@
- - actions status + + actions status - - + Crates.io version - + docs.rs docs - - Download + + Download
## Overview -The PVC-Snapshotter Tool is a Rust-based utility that allows Kubernetes users to backup and restore Persistent Volume Claim (PVC) snapshots. The tool provides robust mechanisms to back up data to AWS Elastic Block Store (EBS) and restore it to any Kubernetes namespace. This tool is designed to work with Kubernetes VolumeSnapshot resources, making backup and restoration operations seamless. +The SnapKube Tool is a Rust-based utility that allows Kubernetes users to backup and restore Persistent Volume Claim (PVC) snapshots. The tool provides robust mechanisms to back up data to AWS Elastic Block Store (EBS) and restore it to any Kubernetes namespace. This tool is designed to work with Kubernetes VolumeSnapshot resources, making backup and restoration operations seamless. The tool supports three primary modes of operation: @@ -33,12 +33,6 @@ The tool supports three primary modes of operation: Restore: Restore PVCs from existing snapshots. Full: Run both backup and restore operations in a single process. - -
-> [!NOTE] -> Average percentage of time saved by PVC-Snapshotter compared to [Velero](https://github.com/vmware-tanzu/velero): **X%**. -
- ## Features - **Backup**: Create Kubernetes VolumeSnapshots from existing PVCs - **Restore**: Restore PVCs to any namespace from a VolumeSnapshot @@ -48,7 +42,7 @@ The tool supports three primary modes of operation: - **Error Handling**: Robust error handling and retries to ensure operations complete reliably ## Prerequisites -Before using PVC-Snapshotter, please ensure you have the following: +Before using SnapKube, please ensure you have the following: - You need Rust installed to compile the tool. Install Rust via rustup - An AWS Account with the appropriate access policy - AWS EBS CSI Driver: Required to be installed in your Kubernetes cluster, which is a CSI Driver to manage the lifecycle of EBS Volumes @@ -61,7 +55,7 @@ In order to use the tool as a client, you can use `cargo`. The tool provides 3 features for running it, which are `backup` `restore`, and `full` (default). ```shell -Usage: pvc-snapshotter-client full [OPTIONS] --source-ns --target-ns --volume-snapshot-class --volume-snapshot-name-prefix --target-snapshot-content-name-prefix --storage-class-name +Usage: snap-kube-client full [OPTIONS] --source-ns --target-ns --volume-snapshot-class --volume-snapshot-name-prefix --target-snapshot-content-name-prefix --storage-class-name Options: --region @@ -113,7 +107,7 @@ cargo run --no-default-features --features restore -- restore ```shell cargo fmt --all cargo clippy --all - +cargo nextest run --all cargo build RUST_LOG=info \ diff --git a/pvc-snapshotter-client/Cargo.toml b/snap-kube-client/Cargo.toml similarity index 66% rename from pvc-snapshotter-client/Cargo.toml rename to snap-kube-client/Cargo.toml index a11dedf..fc0aa77 100644 --- a/pvc-snapshotter-client/Cargo.toml +++ b/snap-kube-client/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "pvc-snapshotter-client" +name = "snap-kube-client" version = "0.1.0" edition = "2021" license = "MIT" -description = "The pvc-snapshotter-client is a Rust tool that can backup and restore k8s PVCs using EBS snapshots" +description = "The snap-kube-client is a Rust tool that can backup and restore k8s PVCs using EBS snapshots" readme = "../README.md" -homepage = "https://github.com/nikoshet/pvc-snapshotter" -repository = "https://github.com/nikoshet/pvc-snapshotter" +homepage = "https://github.com/nikoshet/snap-kube" +repository = "https://github.com/nikoshet/snap-kube" keywords = ["k8s", "pvc", "snapshot", "ebs", "backup", "restore", "aws"] -documentation = "https://docs.rs/pvc-snapshotter-client" +documentation = "https://docs.rs/snap-kube-client" [dependencies] anyhow.workspace = true @@ -27,7 +27,8 @@ serde_json.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true -pvc-snapshotter.workspace = true +snap-kube.workspace = true +mockall = "0.13.0" [features] default = ["full"] diff --git a/pvc-snapshotter-client/src/main.rs b/snap-kube-client/src/main.rs similarity index 97% rename from pvc-snapshotter-client/src/main.rs rename to snap-kube-client/src/main.rs index a1bf1a0..668c195 100644 --- a/pvc-snapshotter-client/src/main.rs +++ b/snap-kube-client/src/main.rs @@ -2,11 +2,11 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use colored::Colorize; #[cfg(feature = "backup")] -use pvc_snapshotter::backup::{backup_operator::BackupOperator, backup_payload::BackupPayload}; +use snap_kube::backup::{backup_operator::BackupOperator, backup_payload::BackupPayload}; #[cfg(feature = "restore")] -use pvc_snapshotter::k8s_ops::vsc::retain_policy::VSCRetainPolicy; +use snap_kube::k8s_ops::vsc::retain_policy::VSCRetainPolicy; #[cfg(feature = "restore")] -use pvc_snapshotter::restore::{ +use snap_kube::restore::{ restore_operator::RestoreOperator, restore_payload::RestorePayload, }; use tracing::info; diff --git a/src/backup/backup_operator.rs b/src/backup/backup_operator.rs index 11e805b..c936d2f 100644 --- a/src/backup/backup_operator.rs +++ b/src/backup/backup_operator.rs @@ -2,14 +2,15 @@ use super::backup_payload::BackupPayload; use crate::{ aws_ops::ebs::create_ebs_client, k8s_ops::{ - pvc::persistent_volume_claims::{check_if_pvc_exists, get_pvcs_available}, - vs::volume_snapshots::wait_untill_snapshot_is_ready, - vs::volume_snapshots_operator::VolumeSnapshotOperator, + pvc::persistent_volume_claims::{check_if_pvc_exists, get_pvcs_available, KubePvcApi}, + vs::{ + volume_snapshots::wait_untill_snapshot_is_ready, + volume_snapshots_operator::VolumeSnapshotOperator, + }, vsc::retain_policy::VSCRetainPolicy, }, }; use anyhow::{bail, Result}; -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, @@ -34,7 +35,9 @@ impl BackupOperator { // Define the VolumeSnapshot and VolumeSnapshotContent APIs let restore_k8s_apis_struct = BackupKubernetesApisStruct { source_vs_api: Api::namespaced(k8s_client.clone(), backup_payload.source_ns()), - source_pvcs_api: Api::namespaced(k8s_client.clone(), backup_payload.source_ns()), + source_pvcs_api: KubePvcApi { + api: Api::namespaced(k8s_client.clone(), backup_payload.source_ns()), + }, vsc_api: Api::all(k8s_client.clone()), }; @@ -102,6 +105,6 @@ impl BackupOperator { /// A struct for holding the Kubernetes APIs for the backup operation struct BackupKubernetesApisStruct { source_vs_api: Api, - source_pvcs_api: Api, + source_pvcs_api: KubePvcApi, vsc_api: Api, } diff --git a/src/k8s_ops/pvc/persistent_volume_claims.rs b/src/k8s_ops/pvc/persistent_volume_claims.rs index 2b1fb9c..f2435b7 100644 --- a/src/k8s_ops/pvc/persistent_volume_claims.rs +++ b/src/k8s_ops/pvc/persistent_volume_claims.rs @@ -1,14 +1,51 @@ use anyhow::Result; +use async_trait::async_trait; use k8s_openapi::api::core::v1::PersistentVolumeClaim; -use kube::api::ListParams; +use kube::{api::ListParams, Api}; use tracing::info; +#[cfg(test)] +use mockall::automock; + +#[cfg_attr(test, automock)] +#[async_trait] +pub trait PvcApiTrait { + async fn list_pvcs(&self) -> Result>; + async fn get(&self, name: &str) -> Result; + async fn create(&self, pvc: PersistentVolumeClaim) -> Result; +} + +pub struct KubePvcApi { + pub api: Api, +} + +/// Implement the PvcApi trait for PVC Api +/// This will allow us to call the functions defined in the PvcApi trait on an instance of KubePvcApi. +/// This is useful for testing, as we can mock the KubePvcApi struct and implement the PvcApi trait +/// to return mock data. +/// This way, we can test the functions that use the KubePvcApi struct without actually making +/// calls to the Kubernetes API. +#[async_trait] +impl PvcApiTrait for KubePvcApi { + async fn list_pvcs(&self) -> Result> { + let pvcs = self.api.list(&ListParams::default()).await?; + Ok(pvcs.items) + } + + async fn get(&self, name: &str) -> Result { + let pvc = self.api.get(name).await?; + Ok(pvc) + } + + async fn create(&self, pvc: PersistentVolumeClaim) -> Result { + let pvc = self.api.create(&Default::default(), &pvc).await?; + Ok(pvc) + } +} + /// 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 { +pub async fn get_pvcs_available(pvc_api: &impl PvcApiTrait) -> Result> { + let pvc_list: Vec<_> = match pvc_api.list_pvcs().await { Ok(pvc) => pvc, Err(e) => panic!("Failed to list PVCs: {}", e), } @@ -20,7 +57,8 @@ pub async fn get_pvcs_available( } pub async fn check_if_pvc_exists( - target_pvc_api: &kube::Api, + target_pvc_api: &impl PvcApiTrait, + //&kube::Api, pvc_name: &str, should_exist: bool, ) -> Result> { @@ -38,7 +76,7 @@ pub async fn check_if_pvc_exists( Ok(Some(pvc)) } else { panic!( - "PVC exists: {} on target namespace {:?}", + "PVC does not exist: {} on target namespace {:?}", pvc.metadata.name.clone().unwrap(), pvc.metadata.namespace.clone().unwrap() ); diff --git a/src/k8s_ops/pvc/persistent_volume_claims_operator.rs b/src/k8s_ops/pvc/persistent_volume_claims_operator.rs index affb297..ae017bc 100644 --- a/src/k8s_ops/pvc/persistent_volume_claims_operator.rs +++ b/src/k8s_ops/pvc/persistent_volume_claims_operator.rs @@ -9,6 +9,26 @@ use k8s_openapi::{ use kube::api::ObjectMeta; use std::collections::BTreeMap; +enum PVCResourceValues { + AccessModes, + K8sKind, + ApiGroup, + VolumeMode, + StorageClass, +} + +impl PVCResourceValues { + pub fn get_value(&self) -> String { + match self { + PVCResourceValues::AccessModes => "ReadWriteOnce".to_string(), + PVCResourceValues::K8sKind => "VolumeSnapshot".to_string(), + PVCResourceValues::ApiGroup => "snapshot.storage.k8s.io".to_string(), + PVCResourceValues::VolumeMode => "Filesystem".to_string(), + PVCResourceValues::StorageClass => "gp3".to_string(), + } + } +} + pub struct PVCOperator { pvc_operator_payload: PVCOperatorPayload, } @@ -36,13 +56,11 @@ impl PVCOperator { /// 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(), + let labels = BTreeMap::from([( + "snap-kube/volume-snapshot-name".to_string(), self.pvc_operator_payload.pvc_name().to_string(), - ); + )]); PersistentVolumeClaim { metadata: ObjectMeta { @@ -55,26 +73,26 @@ impl PVCOperator { access_modes: Some( self.pvc_operator_payload .access_modes() - .unwrap_or(vec!["ReadWriteOnce".to_string()]), + .unwrap_or(vec![PVCResourceValues::AccessModes.get_value()]), ), storage_class_name: Some( self.pvc_operator_payload .storage_class() - .unwrap_or("gp3") + .unwrap_or(&PVCResourceValues::StorageClass.get_value()) .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()), + kind: PVCResourceValues::K8sKind.get_value(), + api_group: Some(PVCResourceValues::ApiGroup.get_value()), }), 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()), + kind: PVCResourceValues::K8sKind.get_value(), + api_group: Some(PVCResourceValues::ApiGroup.get_value()), namespace: Some(String::from(self.pvc_operator_payload.namespace())), }), - volume_mode: Some("Filesystem".to_string()), + volume_mode: Some(PVCResourceValues::VolumeMode.get_value()), volume_name: Default::default(), resources: Some(VolumeResourceRequirements { requests: Some(BTreeMap::from([( diff --git a/src/k8s_ops/pvc/persistent_volume_claims_tests.rs b/src/k8s_ops/pvc/persistent_volume_claims_tests.rs index 4b8088a..9e59a2a 100644 --- a/src/k8s_ops/pvc/persistent_volume_claims_tests.rs +++ b/src/k8s_ops/pvc/persistent_volume_claims_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use crate::k8s_ops::pvc::{ - persistent_volume_claims::get_pvcs_available, + persistent_volume_claims::{MockPvcApiTrait, PvcApiTrait}, persistent_volume_claims_operator::PVCOperator, persistent_volume_claims_payload::PVCOperatorPayload, }; @@ -12,18 +12,19 @@ mod tests { }, apimachinery::pkg::api::resource::Quantity, }; - use kube::{api::ObjectMeta, Api, Client}; + use kube::api::ObjectMeta; + use mockall::predicate; use pretty_assertions::assert_eq; use std::collections::BTreeMap; #[test] fn test_construct_persistent_volume_claim_resource() { let pvc_operator_payload = PVCOperatorPayload::new( - "test-pvc".to_string(), - "test-ns".to_string(), - "gp3".to_string(), - Some(vec!["ReadWriteOnce".to_string()]), - "test-vs".to_string(), + String::from("test-pvc"), + String::from("test-ns"), + String::from("gp3"), + Some(vec![String::from("ReadWriteOnce")]), + String::from("test-vs"), "1Gi".to_string(), ); @@ -31,11 +32,10 @@ mod tests { let pvc = pvc_operator.construct_persistent_volume_claim_resource(); - let mut labels = BTreeMap::new(); - labels.insert( - "pvc-snapshotter/volume-snapshot-name".to_string(), + let labels = BTreeMap::from([( + "snap-kube/volume-snapshot-name".to_string(), "test-pvc".to_string(), - ); + )]); let expected_pvc = PersistentVolumeClaim { metadata: ObjectMeta { @@ -77,18 +77,111 @@ mod tests { } #[tokio::test] - async fn test_get_pvcs_available() { - let client = Client::try_default().await.unwrap(); - let api: Api = Api::namespaced(client, "test-ns"); - let pvcs = get_pvcs_available(&api).await.unwrap(); - assert!(pvcs.is_empty()); + async fn test_create_pvc() { + let mut mock_pvc_api = MockPvcApiTrait::new(); + + let pvc = PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some("test-pvc".to_string()), + namespace: Some("test-ns".to_string()), + ..Default::default() + }, + ..Default::default() + }; + + mock_pvc_api + .expect_create() + .with(predicate::eq(pvc.clone())) + .times(1) + .returning(|_| { + Ok(PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some("test-pvc".to_string()), + namespace: Some("test-ns".to_string()), + ..Default::default() + }, + ..Default::default() + }) + }); + + let result = mock_pvc_api.create(pvc).await; + assert!(result.is_ok()); + assert_eq!( + result.as_ref().cloned().unwrap().metadata.name.unwrap(), + "test-pvc" + ); + assert_eq!(result.unwrap().metadata.namespace.unwrap(), "test-ns"); } #[tokio::test] - async fn test_check_if_pvc_exists() { - let client = Client::try_default().await.unwrap(); - let api: Api = Api::namespaced(client, "test-ns"); - let pvc = api.get("test-pvc").await; - assert!(pvc.is_err()); + async fn test_get_pvc() { + let mut mock_pvc_api = MockPvcApiTrait::new(); + + mock_pvc_api + .expect_get() + .with(predicate::eq("test-pvc")) + .times(1) + .returning(|_| { + Ok(PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some("test-pvc".to_string()), + namespace: Some("test-ns".to_string()), + ..Default::default() + }, + ..Default::default() + }) + }); + + let result = mock_pvc_api.get("test-pvc").await; + assert!(result.is_ok()); + assert_eq!( + result.as_ref().cloned().unwrap().metadata.name.unwrap(), + "test-pvc" + ); + assert_eq!(result.unwrap().metadata.namespace.unwrap(), "test-ns"); + } + + #[tokio::test] + async fn test_list_pvcs() { + let mut mock_pvc_api = MockPvcApiTrait::new(); + + mock_pvc_api.expect_list_pvcs().times(1).returning(|| { + Ok(vec![PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some("test-pvc".to_string()), + namespace: Some("test-ns".to_string()), + ..Default::default() + }, + ..Default::default() + }]) + }); + + let result = mock_pvc_api.list_pvcs().await; + + assert!(result.is_ok()); + assert_eq!(result.as_ref().unwrap().len(), 1); + assert_eq!( + result + .as_ref() + .unwrap() + .get(0) + .unwrap() + .metadata + .name + .clone() + .unwrap(), + "test-pvc" + ); + assert_eq!( + result + .unwrap() + .get(0) + .unwrap() + .metadata + .namespace + .clone() + .unwrap(), + "test-ns" + ); } } diff --git a/src/k8s_ops/vs/volume_snapshots_operator.rs b/src/k8s_ops/vs/volume_snapshots_operator.rs index a8ad22b..30f563b 100644 --- a/src/k8s_ops/vs/volume_snapshots_operator.rs +++ b/src/k8s_ops/vs/volume_snapshots_operator.rs @@ -6,6 +6,20 @@ use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshots::{ VolumeSnapshot, VolumeSnapshotSource, VolumeSnapshotSpec, }; +enum VSResourceValues { + Finalizers, +} + +impl VSResourceValues { + pub fn get_value(&self) -> String { + match self { + VSResourceValues::Finalizers => { + "snapshot.storage.kubernetes.io/volumesnapshot-bound-protection".to_string() + } + } + } +} + pub struct VolumeSnapshotOperator { pub name: String, pub namespace: String, @@ -52,50 +66,43 @@ impl VolumeSnapshotOperator { restore_size: Option, vsc_retain_policy: VSCRetainPolicy, ) -> 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(), - vsc_retain_policy.to_string(), - ); + // Create a base annotations map with always-included entries + let mut annotations = BTreeMap::from([ + ( + "snap-kube/csi-driver-name".into(), + "ebs.csi.aws.com".into(), + ), + ( + "snap-kube/csi-vsc-deletion-policy".into(), + vsc_retain_policy.to_string(), + ), + ]); // 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, - ); + annotations.insert("snap-kube/csi-volumesnapshot-handle".into(), 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(), + "snap-kube/csi-volumesnapshot-restore-size".into(), size, ); } // Create a base labels map - let mut labels = BTreeMap::new(); // Always add the namespace name - labels.insert( + let labels = BTreeMap::from([( "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(), - ]), + finalizers: Some(vec![VSResourceValues::Finalizers.get_value()]), labels: Some(labels), ..Default::default() }, diff --git a/src/k8s_ops/vs/volume_snapshots_tests.rs b/src/k8s_ops/vs/volume_snapshots_tests.rs index edd2496..5840ee4 100644 --- a/src/k8s_ops/vs/volume_snapshots_tests.rs +++ b/src/k8s_ops/vs/volume_snapshots_tests.rs @@ -30,7 +30,7 @@ mod tests { .annotations .as_ref() .unwrap() - .get("pvc-snapshotter/csi-driver-name"), + .get("snap-kube/csi-driver-name"), Some(&"ebs.csi.aws.com".to_string()) ); assert_eq!( @@ -39,7 +39,7 @@ mod tests { .annotations .as_ref() .unwrap() - .get("pvc-snapshotter/csi-vsc-deletion-policy"), + .get("snap-kube/csi-vsc-deletion-policy"), Some(&"Delete".to_string()) ); assert_eq!( @@ -48,7 +48,7 @@ mod tests { .annotations .as_ref() .unwrap() - .get("pvc-snapshotter/csi-volumesnapshot-handle"), + .get("snap-kube/csi-volumesnapshot-handle"), Some(&"test-snapshot-handle".to_string()) ); assert_eq!( @@ -56,7 +56,7 @@ mod tests { .metadata .annotations .unwrap() - .get("pvc-snapshotter/csi-volumesnapshot-restore-size"), + .get("snap-kube/csi-volumesnapshot-restore-size"), Some(&"1Gi".to_string()) ); assert_eq!( diff --git a/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs b/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs index b2859d3..b912d9b 100644 --- a/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs +++ b/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs @@ -1,10 +1,27 @@ +use super::retain_policy::VSCRetainPolicy; use kube::api::ObjectMeta; use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::{ VolumeSnapshotContent, VolumeSnapshotContentSource, VolumeSnapshotContentSpec, VolumeSnapshotContentStatus, VolumeSnapshotContentVolumeSnapshotRef, }; -use super::retain_policy::VSCRetainPolicy; +enum VSCResourceValues { + ApiVersion, + Kind, + Driver, + SourceVolumeMode, +} + +impl VSCResourceValues { + pub fn get_value(&self) -> String { + match self { + VSCResourceValues::ApiVersion => "snapshot.storage.k8s.io/v1".to_string(), + VSCResourceValues::Kind => "VolumeSnapshot".to_string(), + VSCResourceValues::Driver => "ebs.csi.aws.com".to_string(), + VSCResourceValues::SourceVolumeMode => "Filesystem".to_string(), + } + } +} pub struct VolumeSnapshotContentOperator { pub name: String, @@ -56,8 +73,8 @@ impl VolumeSnapshotContentOperator { }, spec: VolumeSnapshotContentSpec { volume_snapshot_ref: VolumeSnapshotContentVolumeSnapshotRef { - api_version: Some("snapshot.storage.k8s.io/v1".to_string()), - kind: Some("VolumeSnapshot".to_string()), + api_version: Some(VSCResourceValues::ApiVersion.get_value()), + kind: Some(VSCResourceValues::Kind.get_value()), name: Some(self.volume_snapshot_name.clone()), namespace: Some(self.namespace.clone()), field_path: Default::default(), @@ -65,13 +82,13 @@ impl VolumeSnapshotContentOperator { uid: Default::default(), }, deletion_policy: self.vsc_retain_policy.into(), - driver: "ebs.csi.aws.com".to_string(), + driver: VSCResourceValues::Driver.get_value(), 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()), + source_volume_mode: Some(VSCResourceValues::SourceVolumeMode.get_value()), }, status: Some(VolumeSnapshotContentStatus { snapshot_handle: self.source_volume_handle.clone(), diff --git a/src/restore/restore_operator.rs b/src/restore/restore_operator.rs index ad17086..ab1d2cd 100644 --- a/src/restore/restore_operator.rs +++ b/src/restore/restore_operator.rs @@ -1,15 +1,16 @@ use crate::k8s_ops::{ pvc::{ - persistent_volume_claims::{check_if_pvc_exists, get_pvcs_available}, + persistent_volume_claims::{check_if_pvc_exists, get_pvcs_available, KubePvcApi}, persistent_volume_claims_operator::PVCOperator, persistent_volume_claims_payload::PVCOperatorPayload, }, vs::volume_snapshots_operator::VolumeSnapshotOperator, - vsc::volume_snapshot_contents::get_snapshot_handle, - vsc::volume_snapshot_contents_operator::VolumeSnapshotContentOperator, + vsc::{ + volume_snapshot_contents::get_snapshot_handle, + volume_snapshot_contents_operator::VolumeSnapshotContentOperator, + }, }; use anyhow::{bail, Result}; -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, @@ -31,9 +32,13 @@ impl RestoreOperator { // Define the VolumeSnapshot, VolumeSnapshotContent and PersistentVolumeClaim APIs let restore_k8s_apis_struct = RestoreKubernetesApisStruct { source_vs_api: Api::namespaced(k8s_client.clone(), restore_payload.source_ns()), - source_pvcs_api: Api::namespaced(k8s_client.clone(), restore_payload.source_ns()), + source_pvcs_api: KubePvcApi { + api: Api::namespaced(k8s_client.clone(), restore_payload.source_ns()), + }, target_vs_api: Api::namespaced(k8s_client.clone(), restore_payload.target_ns()), - target_pvcs_api: Api::namespaced(k8s_client.clone(), restore_payload.target_ns()), + target_pvcs_api: KubePvcApi { + api: Api::namespaced(k8s_client.clone(), restore_payload.target_ns()), + }, vsc_api: Api::all(k8s_client.clone()), }; @@ -140,6 +145,7 @@ impl RestoreOperator { let pp = PostParams::default(); match restore_k8s_apis_struct .target_pvcs_api + .api .create(&pp, &pvc) .await { @@ -155,8 +161,8 @@ impl RestoreOperator { /// A struct for holding the Kubernetes APIs for the restore operation struct RestoreKubernetesApisStruct { source_vs_api: Api, - source_pvcs_api: Api, + source_pvcs_api: KubePvcApi, target_vs_api: Api, - target_pvcs_api: Api, + target_pvcs_api: KubePvcApi, vsc_api: Api, }