From 6292c9f5e0701844a9eb05a229434545e32618a3 Mon Sep 17 00:00:00 2001 From: nikoshet Date: Wed, 16 Oct 2024 19:21:50 +0300 Subject: [PATCH] Updates --- .github/workflows/ci.yaml | 16 +- .github/workflows/tests-workflow.yaml | 24 ++ Cargo.lock | 159 +++++++-- Cargo.toml | 50 ++- Dockerfile | 22 -- LICENSE | 21 ++ README.md | 126 ++++++- scripts/backup-docker-entrypoint.sh | 7 - scripts/full-docker-entrypoint.sh | 10 - scripts/restore-docker-entrypoint.sh | 10 - snap-kube-client/Cargo.toml | 37 ++ snap-kube-client/src/main.rs | 241 +++++++++++++ src/aws_ops/ebs.rs | 2 +- src/aws_ops/mod.rs | 2 + src/backup/backup_operator.rs | 212 +++++------- src/backup/backup_payload.rs | 66 ++++ src/backup/mod.rs | 3 + src/k8s_ops/mod.rs | 6 +- src/k8s_ops/persistent_volume_claims.rs | 77 ----- src/k8s_ops/pvc/mod.rs | 8 + src/k8s_ops/pvc/persistent_volume_claims.rs | 94 ++++++ .../pvc/persistent_volume_claims_operator.rs | 110 ++++++ .../pvc/persistent_volume_claims_payload.rs | 66 ++++ .../pvc/persistent_volume_claims_tests.rs | 187 +++++++++++ src/k8s_ops/volume_snapshot_contents.rs | 85 ----- src/k8s_ops/volume_snapshots.rs | 137 -------- src/k8s_ops/vs/mod.rs | 6 + src/k8s_ops/vs/volume_snapshots.rs | 61 ++++ src/k8s_ops/vs/volume_snapshots_operator.rs | 113 +++++++ src/k8s_ops/vs/volume_snapshots_tests.rs | 91 +++++ src/k8s_ops/vsc/mod.rs | 7 + src/k8s_ops/vsc/retain_policy.rs | 53 +++ src/k8s_ops/vsc/volume_snapshot_contents.rs | 26 ++ .../vsc/volume_snapshot_contents_operator.rs | 103 ++++++ .../vsc/volume_snapshot_contents_tests.rs | 61 ++++ src/lib.rs | 4 + src/main.rs | 167 --------- src/restore/mod.rs | 3 + src/restore/restore_operator.rs | 317 ++++++++---------- src/restore/restore_payload.rs | 76 +++++ 40 files changed, 2013 insertions(+), 853 deletions(-) create mode 100644 .github/workflows/tests-workflow.yaml delete mode 100644 Dockerfile create mode 100644 LICENSE delete mode 100755 scripts/backup-docker-entrypoint.sh delete mode 100755 scripts/full-docker-entrypoint.sh delete mode 100755 scripts/restore-docker-entrypoint.sh create mode 100644 snap-kube-client/Cargo.toml create mode 100644 snap-kube-client/src/main.rs create mode 100644 src/backup/backup_payload.rs delete mode 100644 src/k8s_ops/persistent_volume_claims.rs create mode 100644 src/k8s_ops/pvc/mod.rs create mode 100644 src/k8s_ops/pvc/persistent_volume_claims.rs create mode 100644 src/k8s_ops/pvc/persistent_volume_claims_operator.rs create mode 100644 src/k8s_ops/pvc/persistent_volume_claims_payload.rs create mode 100644 src/k8s_ops/pvc/persistent_volume_claims_tests.rs delete mode 100644 src/k8s_ops/volume_snapshot_contents.rs delete mode 100644 src/k8s_ops/volume_snapshots.rs create mode 100644 src/k8s_ops/vs/mod.rs create mode 100644 src/k8s_ops/vs/volume_snapshots.rs create mode 100644 src/k8s_ops/vs/volume_snapshots_operator.rs create mode 100644 src/k8s_ops/vs/volume_snapshots_tests.rs create mode 100644 src/k8s_ops/vsc/mod.rs create mode 100644 src/k8s_ops/vsc/retain_policy.rs create mode 100644 src/k8s_ops/vsc/volume_snapshot_contents.rs create mode 100644 src/k8s_ops/vsc/volume_snapshot_contents_operator.rs create mode 100644 src/k8s_ops/vsc/volume_snapshot_contents_tests.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs create mode 100644 src/restore/restore_payload.rs 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 cf69239..c8610a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,6 +752,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -763,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" @@ -832,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" @@ -1511,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" @@ -1734,32 +1778,48 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.86" +name = "predicates" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ - "unicode-ident", + "anstyle", + "predicates-core", ] [[package]] -name = "pvc-snapshotter" -version = "0.1.0" +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 = [ - "anyhow", - "aws-config", - "aws-sdk-ec2", - "clap", - "colored", - "k8s-openapi", - "kube", - "kube-custom-resources-rs", - "schemars", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", ] [[package]] @@ -2206,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" @@ -2256,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" @@ -2790,6 +2903,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index b854da7..8f6e858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,22 @@ [package] -name = "pvc-snapshotter" +name = "snap-kube" version = "0.1.0" edition = "2021" +license = "MIT" +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/snap-kube" +repository = "https://github.com/nikoshet/snap-kube" +keywords = ["k8s", "pvc", "snapshot", "ebs", "backup", "restore", "aws"] +documentation = "https://docs.rs/snap-kube" +exclude = ["script.sh"] -[dependencies] +[workspace] +members = ["snap-kube-client"] + +[workspace.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"] } @@ -12,9 +24,43 @@ colored = "2.1.0" k8s-openapi = { version = "0.23.0", features = ["v1_29"] } kube = { version = "0.95.0", features = ["runtime", "derive"] } kube-custom-resources-rs = { version = "2024.6.1", features = ["snapshot_storage_k8s_io"] } +pretty_assertions = "1.4.1" schemars = "0.8.21" serde = "1.0.210" serde_json = "1.0.128" tokio = { version = "1", features = ["full"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" +snap-kube = { path = ".", version = "0.1" } +mockall = "0.13" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +aws-config.workspace = true +aws-sdk-ec2.workspace = true +clap.workspace = true +colored.workspace = true +k8s-openapi.workspace = true +kube.workspace = true +kube-custom-resources-rs.workspace = true +pretty_assertions.workspace = true +schemars.workspace = true +serde.workspace = true +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 = "snap_kube" + +[features] +default = ["full"] +full = ["backup", "restore"] +backup = [] +restore = [] diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index bef95f9..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# Use a Rust base image -ARG RUST_VERSION=1.81.0 -ARG IMAGE_NAME="public.ecr.aws/docker/library/rust:${RUST_VERSION}-slim-bookworm" - -FROM $IMAGE_NAME AS builder -RUN apt-get update && apt-get install -y libssl-dev pkg-config - -# Copy the Rust code into the container -COPY . . - -# Build the Rust application -RUN cargo build --locked --release --bin pvc-snapshotter && \ - mkdir -p /bin/pvc-snapshotter/ && \ - cp ./target/release/pvc-snapshotter /bin/pvc-snapshotter/ - -FROM $IMAGE_NAME AS runtime - -# Set the working directory -WORKDIR /app -RUN apt-get update -COPY --from=builder /bin/pvc-snapshotter/pvc-snapshotter /usr/local/bin/pvc-snapshotter -COPY scripts /app/scripts diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aac5a1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Nick Nikitas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bf6022c..98efd78 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,125 @@ -# PVC-SNAPSHOTTER +

SnapKube

+
+ + A Rust 🦀 tool supports (for now) PVC snapshots across Kubernetes namespaces + +
-Rust tool that supports PVC snapshots across Kubernetes namespaces (Velerust/Rustero) +
+ +
+ + + actions status + + + Crates.io version + + + docs.rs docs + + + Download + +
+ +## Overview +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: + + Backup: Create snapshots of one or more PVCs. + Restore: Restore PVCs from existing snapshots. + Full: Run both backup and restore operations in a single process. + +## Features +- **Backup**: Create Kubernetes VolumeSnapshots from existing PVCs +- **Restore**: Restore PVCs to any namespace from a VolumeSnapshot +- **Flexible Configuration**: The user can either snapshot a specific PVC, or all the PVCs in a specific namespace using the relative flags +- **AWS EBS Integration**: Natively supports backup and restoration to AWS Elastic Block Store +- **Conditional Compilation**: Enable or disable specific modes (backup, restore, full) via Rust feature flags, optimizing binary size and performance. +- **Error Handling**: Robust error handling and retries to ensure operations complete reliably + +## Prerequisites +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 +- A snapshot-controller that supports handling the VolumeSnapshot and VolumeSnapshotContent Objects +- A specific VolumeSnapshotClass for the CSI driver +- Kubernetes CLI + +## Installation (Client) +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: 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 + Region where the EBS volumes are stored [default: eu-west-1] + --source-ns + Source namespace + --target-ns + Target namespace + --volume-snapshot-class + VolumeSnapshotClass name + --pvc-name + PVC name [default: ] + --include-all + Include all PVCs in the namespace + --volume-snapshot-name-prefix + VolumeSnapshot name prefix + --target-snapshot-content-name-prefix + Target VolumeSnapshotContent name prefix + --storage-class-name + StorageClass name + --vsc-retain-policy + VSC Retain Policy [default: delete] [possible values: retain, delete] + -h, --help + Print help + -V, --version + Print version +``` + +## Installation (Library) +Run the tool: +- For **full** mode: +``` +cargo run full +``` + +- For **backup** mode: +``` +cargo run --no-default-features --features backup -- backup +``` + +- For **restore** mode: +``` +cargo run --no-default-features --features restore -- restore +``` + +## Example + +- Build and run the Rust tool +```shell +cargo fmt --all +cargo clippy --all +cargo nextest run --all +cargo build + +RUST_LOG=info \ + cargo run full \ + --source-ns "source-ns" \ + --target-ns "target-ns" \ + --volume-snapshot-class "volumesnapshotclass-name" \ + --include-all \ + --volume-snapshot-name-prefix "prefix-vs" \ + --target-snapshot-content-name-prefix "prefix-vsc" \ + --storage-class-name "ebs-test-sc" +``` + +## License +This project is licensed under the MIT License diff --git a/scripts/backup-docker-entrypoint.sh b/scripts/backup-docker-entrypoint.sh deleted file mode 100755 index 34125be..0000000 --- a/scripts/backup-docker-entrypoint.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -pvc-snapshotter backup \ - --source-ns ${SOURCE_NS} \ - --volume-snapshot-class ${VOLUME_SNAPSHOT_CLASS} \ - --pvc-name ${PVC_NAME} \ - --volume-snapshot-name ${VOLUME_SNAPSHOT_NAME} diff --git a/scripts/full-docker-entrypoint.sh b/scripts/full-docker-entrypoint.sh deleted file mode 100755 index a5467d9..0000000 --- a/scripts/full-docker-entrypoint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -pvc-snapshotter full \ - --source-ns ${SOURCE_NS} \ - --target-ns ${TARGET_NS} \ - --volume-snapshot-class ${VOLUME_SNAPSHOT_CLASS} \ - --pvc-name ${PVC_NAME} \ - --volume-snapshot-name ${VOLUME_SNAPSHOT_NAME} \ - --target-snapshot-content-name ${TARGET_SNAPSHOT_CONTENT_NAME} \ - --storage-class-name ${STORAGE_CLASS_NAME} diff --git a/scripts/restore-docker-entrypoint.sh b/scripts/restore-docker-entrypoint.sh deleted file mode 100755 index de07131..0000000 --- a/scripts/restore-docker-entrypoint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -pvc-snapshotter restore \ - --source-ns ${SOURCE_NS} \ - --target-ns ${TARGET_NS} \ - --volume-snapshot-class ${VOLUME_SNAPSHOT_CLASS} \ - --pvc-name ${PVC_NAME} \ - --volume-snapshot-name ${VOLUME_SNAPSHOT_NAME} \ - --target-snapshot-content-name ${TARGET_SNAPSHOT_CONTENT_NAME} \ - --storage-class-name ${STORAGE_CLASS_NAME} diff --git a/snap-kube-client/Cargo.toml b/snap-kube-client/Cargo.toml new file mode 100644 index 0000000..fc0aa77 --- /dev/null +++ b/snap-kube-client/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "snap-kube-client" +version = "0.1.0" +edition = "2021" +license = "MIT" +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/snap-kube" +repository = "https://github.com/nikoshet/snap-kube" +keywords = ["k8s", "pvc", "snapshot", "ebs", "backup", "restore", "aws"] +documentation = "https://docs.rs/snap-kube-client" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +aws-config.workspace = true +aws-sdk-ec2.workspace = true +clap.workspace = true +colored.workspace = true +k8s-openapi.workspace = true +kube.workspace = true +kube-custom-resources-rs.workspace = true +pretty_assertions.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +snap-kube.workspace = true +mockall = "0.13.0" + +[features] +default = ["full"] +full = ["backup", "restore"] +backup = [] +restore = [] diff --git a/snap-kube-client/src/main.rs b/snap-kube-client/src/main.rs new file mode 100644 index 0000000..6821ace --- /dev/null +++ b/snap-kube-client/src/main.rs @@ -0,0 +1,241 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use colored::Colorize; +#[cfg(feature = "backup")] +use snap_kube::backup::{backup_operator::BackupOperator, backup_payload::BackupPayload}; +#[cfg(feature = "restore")] +use snap_kube::k8s_ops::vsc::retain_policy::VSCRetainPolicy; +#[cfg(feature = "restore")] +use snap_kube::restore::{restore_operator::RestoreOperator, restore_payload::RestorePayload}; +use tracing::info; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + #[cfg(feature = "backup")] + Backup { + /// Region where the EBS volumes are stored + #[arg(long, required = false, default_value = "eu-west-1")] + region: String, + /// Source namespace + #[arg(long, required = true)] + source_ns: String, + /// VolumeSnapshotClass name + #[arg(long, required = true)] + volume_snapshot_class: String, + /// PVC name + #[arg(long, required = false, conflicts_with = "include_all")] + pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "false", + conflicts_with = "pvc_name" + )] + include_all: bool, + /// VolumeSnapshot name prefix + #[arg(long, required = true)] + volume_snapshot_name_prefix: String, + }, + #[cfg(feature = "restore")] + Restore { + /// Source namespace + #[arg(long, required = true)] + source_ns: String, + /// Target namespace + #[arg(long, required = true)] + target_ns: String, + /// VolumeSnapshotClass name + #[arg(long, required = true)] + volume_snapshot_class: String, + /// PVC name + #[arg(long, required = false, conflicts_with = "include_all")] + pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "false", + conflicts_with = "pvc_name" + )] + include_all: bool, + /// VolumeSnapshot name prefix + #[arg(long, required = true)] + volume_snapshot_name_prefix: String, + /// Target VolumeSnapshotContent name prefix + #[arg(long, required = true)] + target_snapshot_content_name_prefix: String, + /// StorageClass name + #[arg(long, required = true)] + storage_class_name: String, + /// VSC Retain Policy + #[arg(long, required = false, default_value = "delete")] + #[clap(value_enum)] + vsc_retain_policy: VSCRetainPolicy, + }, + #[cfg(feature = "full")] + Full { + /// Region where the EBS volumes are stored + #[arg(long, required = false, default_value = "eu-west-1")] + region: String, + /// Source namespace + #[arg(long, required = true)] + source_ns: String, + /// Target namespace + #[arg(long, required = true)] + target_ns: String, + /// VolumeSnapshotClass name + #[arg(long, required = true)] + volume_snapshot_class: String, + /// PVC name + #[arg( + long, + required = false, + conflicts_with = "include_all", + default_value = "" + )] + pvc_name: String, + /// Include all PVCs in the namespace + #[arg( + long, + required = false, + default_value = "false", + conflicts_with = "pvc_name" + )] + include_all: bool, + /// VolumeSnapshot name prefix + #[arg(long, required = true)] + volume_snapshot_name_prefix: String, + /// Target VolumeSnapshotContent name prefix + #[arg(long, required = true)] + target_snapshot_content_name_prefix: String, + /// StorageClass name + #[arg(long, required = true)] + storage_class_name: String, + /// VSC Retain Policy + #[arg(long, required = false, default_value = "delete")] + #[clap(value_enum)] + vsc_retain_policy: VSCRetainPolicy, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let cli = Cli::parse(); + match cli.command { + #[cfg(feature = "backup")] + Commands::Backup { + region, + source_ns, + volume_snapshot_class, + pvc_name, + include_all, + volume_snapshot_name_prefix, + } => { + let backup_payload = BackupPayload::new( + region, + source_ns, + volume_snapshot_class, + pvc_name, + include_all, + volume_snapshot_name_prefix, + ); + + info!("{}", "Starting Backup process...".bold().blue()); + BackupOperator::backup(backup_payload).await?; + info!( + "{}", + "Backup process completed successfully!".bold().green() + ); + } + #[cfg(feature = "restore")] + Commands::Restore { + source_ns, + target_ns, + volume_snapshot_class, + pvc_name, + include_all, + volume_snapshot_name_prefix, + target_snapshot_content_name_prefix, + storage_class_name, + vsc_retain_policy, + } => { + let restore_payload = RestorePayload::new( + source_ns.clone(), + target_ns.clone(), + volume_snapshot_class.clone(), + pvc_name.clone(), + include_all, + volume_snapshot_name_prefix.clone(), + target_snapshot_content_name_prefix.clone(), + storage_class_name.clone(), + vsc_retain_policy, + ); + info!("{}", "Starting Restore process...".bold().blue()); + RestoreOperator::restore(restore_payload).await?; + info!( + "{}", + "Restore process completed successfully!".bold().green() + ); + } + #[cfg(feature = "full")] + Commands::Full { + region, + source_ns, + target_ns, + volume_snapshot_class, + pvc_name, + include_all, + volume_snapshot_name_prefix, + target_snapshot_content_name_prefix, + storage_class_name, + vsc_retain_policy, + } => { + let backup_payload = BackupPayload::new( + region.clone(), + source_ns.clone(), + volume_snapshot_class.clone(), + pvc_name.clone(), + include_all, + volume_snapshot_name_prefix.clone(), + ); + + let restore_payload = RestorePayload::new( + source_ns.clone(), + target_ns.clone(), + volume_snapshot_class.clone(), + pvc_name.clone(), + include_all, + volume_snapshot_name_prefix.clone(), + target_snapshot_content_name_prefix.clone(), + storage_class_name.clone(), + vsc_retain_policy, + ); + + info!("{}", "Starting Backup process...".bold().blue()); + BackupOperator::backup(backup_payload).await?; + info!( + "{}", + "Backup process completed successfully!".bold().green() + ); + + info!("{}", "Starting Restore process...".bold().blue()); + RestoreOperator::restore(restore_payload).await?; + info!( + "{}", + "Restore process completed successfully!".bold().green() + ); + } + }; + Ok(()) +} diff --git a/src/aws_ops/ebs.rs b/src/aws_ops/ebs.rs index 251c8a5..f8cb4ea 100644 --- a/src/aws_ops/ebs.rs +++ b/src/aws_ops/ebs.rs @@ -28,7 +28,7 @@ pub async fn create_ebs_client(region: Option) -> Result { /// /// Progress of the snapshot pub async fn get_ebs_snapshot_progress( - ebs_client: &EbsClient, + ebs_client: EbsClient, snapshot_id: String, ) -> Result { let resp = ebs_client diff --git a/src/aws_ops/mod.rs b/src/aws_ops/mod.rs index 8b98057..36bc242 100644 --- a/src/aws_ops/mod.rs +++ b/src/aws_ops/mod.rs @@ -1,2 +1,4 @@ +#[cfg(feature = "backup")] pub mod ebs; +#[cfg(feature = "backup")] mod region; diff --git a/src/backup/backup_operator.rs b/src/backup/backup_operator.rs index 45381fa..c936d2f 100644 --- a/src/backup/backup_operator.rs +++ b/src/backup/backup_operator.rs @@ -1,3 +1,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, KubePvcApi}, + vs::{ + volume_snapshots::wait_untill_snapshot_is_ready, + volume_snapshots_operator::VolumeSnapshotOperator, + }, + vsc::retain_policy::VSCRetainPolicy, + }, +}; use anyhow::{bail, Result}; use kube::{api::PostParams, Api, Client}; use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::{ @@ -6,149 +18,93 @@ 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, -} +pub struct BackupOperator; 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 - /// - /// # Returns - /// - /// BackupOperator instance - /// - /// # 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(), - /// ); - /// ``` - pub fn new( - region: String, - source_ns: String, - volume_snapshot_class: String, - pvc_name: String, - volume_snapshot_name: String, - ) -> Self { - BackupOperator { - region, - source_ns, - volume_snapshot_class, - pvc_name, - 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<()> { + /// Takes a backup of one or more PVCs from a specific namespace to a VolumeSnapshot/VolumeSnapshotContent + pub async fn backup(backup_payload: BackupPayload) -> Result<()> { // Create a Kubernetes client let k8s_client = Client::try_default().await?; // Create an AWS EBS client - let ebs_client = create_ebs_client(Some(self.region.to_string())); + let ebs_client = create_ebs_client(Some(backup_payload.region().to_string())) + .await + .unwrap(); // Define the VolumeSnapshot and VolumeSnapshotContent APIs - let source_kubernetes_apis_struct = SourceKubernetesApisStruct { - source_vs_api: Api::namespaced(k8s_client.clone(), &self.source_ns), + let restore_k8s_apis_struct = BackupKubernetesApisStruct { + source_vs_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()), }; - // 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, - ); - - 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); - } + // Check if we will backup all PVCs in the namespace + let pvcs = if backup_payload.include_all() { + get_pvcs_available(&restore_k8s_apis_struct.source_pvcs_api).await? + } else { + vec![backup_payload.pvc_name().to_string()] }; - 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 - ) - ); + // We will iterate over the PVCs vector and backup each PVC + for pvc in pvcs { + info!("Backing up PVC: {}", pvc); + let volume_snapshot_name = format!("{}-{}", backup_payload.vs_name_prefix(), pvc); + + // Check if the PVC exists, it should exist + check_if_pvc_exists(&restore_k8s_apis_struct.source_pvcs_api, &pvc, true).await?; + + let vs_operator = VolumeSnapshotOperator::new( + volume_snapshot_name.to_string(), + backup_payload.source_ns.to_string(), + backup_payload.volume_snapshot_class().to_string(), + Some(pvc), + None, + ); + + let volume_snapshot = + vs_operator.construct_volume_snapshot_resource(None, None, VSCRetainPolicy::Delete); + let pp = PostParams::default(); + let status: VolumeSnapshotStatus = match restore_k8s_apis_struct + .source_vs_api + .create(&pp, &volume_snapshot) + .await + { + Ok(snapshot) => { + info!("Created VolumeSnapshot: {:?}", snapshot); + wait_untill_snapshot_is_ready( + &restore_k8s_apis_struct.source_vs_api, + &restore_k8s_apis_struct.vsc_api, + &ebs_client, + &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! VS name: {}, Bound VSC name: {}, Restore size: {}", + volume_snapshot_name, bound_vsc_name, restore_size + ) + ); + } Ok(()) } } + +/// A struct for holding the Kubernetes APIs for the backup operation +struct BackupKubernetesApisStruct { + source_vs_api: Api, + source_pvcs_api: KubePvcApi, + vsc_api: Api, +} diff --git a/src/backup/backup_payload.rs b/src/backup/backup_payload.rs new file mode 100644 index 0000000..d58cd50 --- /dev/null +++ b/src/backup/backup_payload.rs @@ -0,0 +1,66 @@ +pub struct BackupPayload { + pub region: String, + pub source_ns: String, + pub volume_snapshot_class: String, + pub pvc_name: String, + pub include_all: bool, + pub vs_name_prefix: String, +} + +impl BackupPayload { + /// Creates a new BackupPayload + /// + /// # Arguments + /// + /// * `region` - AWS region + /// * `source_ns` - Source namespace + /// * `volume_snapshot_class` - VolumeSnapshotClass name + /// * `pvc_name` - PVC name + /// * `include_all` - Include all PVCs in the namespace + /// * `vs_name_prefix` - VolumeSnapshot name prefix + /// + /// # Returns + /// + /// A new BackupPayload instance + pub fn new( + region: impl Into, + source_ns: impl Into, + volume_snapshot_class: impl Into, + pvc_name: impl Into, + include_all: bool, + vs_name_prefix: impl Into, + ) -> Self { + Self { + region: region.into(), + source_ns: source_ns.into(), + volume_snapshot_class: volume_snapshot_class.into(), + pvc_name: pvc_name.into(), + include_all, + vs_name_prefix: vs_name_prefix.into(), + } + } + + pub fn region(&self) -> &str { + &self.region + } + + pub fn source_ns(&self) -> &str { + &self.source_ns + } + + pub fn volume_snapshot_class(&self) -> &str { + &self.volume_snapshot_class + } + + pub fn pvc_name(&self) -> &str { + &self.pvc_name + } + + pub fn include_all(&self) -> bool { + self.include_all + } + + pub fn vs_name_prefix(&self) -> &str { + &self.vs_name_prefix + } +} diff --git a/src/backup/mod.rs b/src/backup/mod.rs index f2708b0..78a581b 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -1 +1,4 @@ +#[cfg(feature = "backup")] pub mod backup_operator; +#[cfg(feature = "backup")] +pub mod backup_payload; diff --git a/src/k8s_ops/mod.rs b/src/k8s_ops/mod.rs index 0b737aa..f738c13 100644 --- a/src/k8s_ops/mod.rs +++ b/src/k8s_ops/mod.rs @@ -1,3 +1,3 @@ -pub mod persistent_volume_claims; -pub mod volume_snapshot_contents; -pub mod volume_snapshots; +pub mod pvc; +pub mod vs; +pub mod vsc; diff --git a/src/k8s_ops/persistent_volume_claims.rs b/src/k8s_ops/persistent_volume_claims.rs deleted file mode 100644 index a8ca333..0000000 --- a/src/k8s_ops/persistent_volume_claims.rs +++ /dev/null @@ -1,77 +0,0 @@ -use k8s_openapi::{ - api::core::v1::{ - PersistentVolumeClaim, PersistentVolumeClaimSpec, TypedLocalObjectReference, - TypedObjectReference, VolumeResourceRequirements, - }, - apimachinery::pkg::api::resource::Quantity, -}; -use kube::api::ObjectMeta; -use std::collections::BTreeMap; - -/// 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(); - - // Always add the VSc name - labels.insert( - "pvc-snapshotter/volume-snapshot-name".to_string(), - volume_snapshot_name.to_string(), - ); - - 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()), - )])), - ..Default::default() - }), - selector: Default::default(), - volume_attributes_class_name: Default::default(), - }), - ..Default::default() - } -} diff --git a/src/k8s_ops/pvc/mod.rs b/src/k8s_ops/pvc/mod.rs new file mode 100644 index 0000000..d465ab1 --- /dev/null +++ b/src/k8s_ops/pvc/mod.rs @@ -0,0 +1,8 @@ +pub mod persistent_volume_claims; +#[cfg(feature = "restore")] +pub mod persistent_volume_claims_operator; +#[cfg(feature = "restore")] +pub mod persistent_volume_claims_payload; + +#[cfg(test)] +mod persistent_volume_claims_tests; diff --git a/src/k8s_ops/pvc/persistent_volume_claims.rs b/src/k8s_ops/pvc/persistent_volume_claims.rs new file mode 100644 index 0000000..f2435b7 --- /dev/null +++ b/src/k8s_ops/pvc/persistent_volume_claims.rs @@ -0,0 +1,94 @@ +use anyhow::Result; +use async_trait::async_trait; +use k8s_openapi::api::core::v1::PersistentVolumeClaim; +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(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), + } + .into_iter() + .map(|pvc| pvc.metadata.name.unwrap()) + .collect(); + info!("PVCs available: {:?}", pvc_list); + Ok(pvc_list) +} + +pub async fn check_if_pvc_exists( + target_pvc_api: &impl PvcApiTrait, + //&kube::Api, + pvc_name: &str, + should_exist: bool, +) -> Result> { + match target_pvc_api.get(pvc_name).await { + Ok(pvc) => { + if should_exist { + info!( + "{}", + format!( + "PVC exists: {} on target namespace {:?}", + pvc.metadata.name.clone().unwrap(), + pvc.metadata.namespace.clone().unwrap() + ) + ); + Ok(Some(pvc)) + } else { + panic!( + "PVC does not exist: {} on target namespace {:?}", + pvc.metadata.name.clone().unwrap(), + pvc.metadata.namespace.clone().unwrap() + ); + } + } + Err(e) => { + if should_exist { + panic!("Failed to get PVC: {}", e); + } else { + info!("PVC does not exist: {}", pvc_name); + Ok(None) + } + } + } +} diff --git a/src/k8s_ops/pvc/persistent_volume_claims_operator.rs b/src/k8s_ops/pvc/persistent_volume_claims_operator.rs new file mode 100644 index 0000000..ae017bc --- /dev/null +++ b/src/k8s_ops/pvc/persistent_volume_claims_operator.rs @@ -0,0 +1,110 @@ +use super::persistent_volume_claims_payload::PVCOperatorPayload; +use k8s_openapi::{ + api::core::v1::{ + PersistentVolumeClaim, PersistentVolumeClaimSpec, TypedLocalObjectReference, + TypedObjectReference, VolumeResourceRequirements, + }, + apimachinery::pkg::api::resource::Quantity, +}; +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, +} + +impl PVCOperator { + pub fn new(pvc_operator_payload: PVCOperatorPayload) -> Self { + Self { + pvc_operator_payload, + } + } + + /// 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 + // Always add the VSc name + let labels = BTreeMap::from([( + "snap-kube/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![PVCResourceValues::AccessModes.get_value()]), + ), + storage_class_name: Some( + self.pvc_operator_payload + .storage_class() + .unwrap_or(&PVCResourceValues::StorageClass.get_value()) + .to_string(), + ), + data_source: Some(TypedLocalObjectReference { + name: String::from(self.pvc_operator_payload.volume_snapshot_name()), + 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: PVCResourceValues::K8sKind.get_value(), + api_group: Some(PVCResourceValues::ApiGroup.get_value()), + namespace: Some(String::from(self.pvc_operator_payload.namespace())), + }), + volume_mode: Some(PVCResourceValues::VolumeMode.get_value()), + 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(), + }), + ..Default::default() + } + } +} diff --git a/src/k8s_ops/pvc/persistent_volume_claims_payload.rs b/src/k8s_ops/pvc/persistent_volume_claims_payload.rs new file mode 100644 index 0000000..e50c909 --- /dev/null +++ b/src/k8s_ops/pvc/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/pvc/persistent_volume_claims_tests.rs b/src/k8s_ops/pvc/persistent_volume_claims_tests.rs new file mode 100644 index 0000000..9e59a2a --- /dev/null +++ b/src/k8s_ops/pvc/persistent_volume_claims_tests.rs @@ -0,0 +1,187 @@ +#[cfg(test)] +mod tests { + use crate::k8s_ops::pvc::{ + persistent_volume_claims::{MockPvcApiTrait, PvcApiTrait}, + persistent_volume_claims_operator::PVCOperator, + persistent_volume_claims_payload::PVCOperatorPayload, + }; + use k8s_openapi::{ + api::core::v1::{ + PersistentVolumeClaim, PersistentVolumeClaimSpec, TypedLocalObjectReference, + TypedObjectReference, VolumeResourceRequirements, + }, + apimachinery::pkg::api::resource::Quantity, + }; + 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( + String::from("test-pvc"), + String::from("test-ns"), + String::from("gp3"), + Some(vec![String::from("ReadWriteOnce")]), + String::from("test-vs"), + "1Gi".to_string(), + ); + + let pvc_operator = PVCOperator::new(pvc_operator_payload); + + let pvc = pvc_operator.construct_persistent_volume_claim_resource(); + + let labels = BTreeMap::from([( + "snap-kube/volume-snapshot-name".to_string(), + "test-pvc".to_string(), + )]); + + let expected_pvc = PersistentVolumeClaim { + metadata: ObjectMeta { + name: Some("test-pvc".to_string()), + namespace: Some("test-ns".to_string()), + labels: Some(labels), + ..Default::default() + }, + spec: Some(PersistentVolumeClaimSpec { + access_modes: Some(vec!["ReadWriteOnce".to_string()]), + storage_class_name: Some("gp3".to_string()), + data_source: Some(TypedLocalObjectReference { + name: "test-vs".to_string(), + kind: "VolumeSnapshot".to_string(), + api_group: Some("snapshot.storage.k8s.io".to_string()), + }), + data_source_ref: Some(TypedObjectReference { + name: "test-vs".to_string(), + kind: "VolumeSnapshot".to_string(), + api_group: Some("snapshot.storage.k8s.io".to_string()), + namespace: Some("test-ns".to_string()), + }), + volume_mode: Some("Filesystem".to_string()), + volume_name: Default::default(), + resources: Some(VolumeResourceRequirements { + requests: Some(BTreeMap::from([( + "storage".to_string(), + Quantity("1Gi".to_string()), + )])), + ..Default::default() + }), + selector: Default::default(), + volume_attributes_class_name: Default::default(), + }), + ..Default::default() + }; + + assert_eq!(pvc, expected_pvc); + } + + #[tokio::test] + 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_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/volume_snapshot_contents.rs b/src/k8s_ops/volume_snapshot_contents.rs deleted file mode 100644 index be243c3..0000000 --- a/src/k8s_ops/volume_snapshot_contents.rs +++ /dev/null @@ -1,85 +0,0 @@ -use anyhow::{bail, Result}; -use kube::{api::ObjectMeta, Api}; -use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::{ - VolumeSnapshotContent, VolumeSnapshotContentDeletionPolicy, VolumeSnapshotContentSource, - 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(), - ..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(), - }), - } -} - -/// Get the snapshot handle from the VolumeSnapshotContent -/// -/// # Arguments -/// -/// * `vsc_api` - Api object for VolumeSnapshotContent -/// * `volume_snapshot_content_name` - Name of the VolumeSnapshotContent resource -/// -/// # Returns -/// -/// Snapshot handle -pub async fn get_snapshot_handle( - vsc_api: Api, - volume_snapshot_content_name: &str, -) -> Result { - let volume_snapshot_content = vsc_api.get(volume_snapshot_content_name).await?; - - if let Some(status) = volume_snapshot_content.status { - Ok(status.snapshot_handle.unwrap()) - } else { - bail!("Status of VolumeSnapshotContent is not available") - } -} diff --git a/src/k8s_ops/volume_snapshots.rs b/src/k8s_ops/volume_snapshots.rs deleted file mode 100644 index b68af6c..0000000 --- a/src/k8s_ops/volume_snapshots.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::aws_ops::ebs::get_ebs_snapshot_progress; -use crate::k8s_ops::volume_snapshot_contents::get_snapshot_handle; -use anyhow::Result; -use aws_sdk_ec2::Client as EbsClient; -use kube::api::ObjectMeta; -use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::VolumeSnapshotContent; -use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshots::{ - VolumeSnapshot, VolumeSnapshotSource, VolumeSnapshotSpec, VolumeSnapshotStatus, -}; -use std::collections::BTreeMap; -use std::time::Duration; -use tokio::time::sleep; -use tracing::{error, info}; - -/// 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(), - ); - - // 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, - ); - } - - // 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()); - - 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, - }, - }, - ..Default::default() - } -} - -/// Wait untill the VolumeSnapshot is ready -/// -/// # Arguments -/// -/// * `vs_api` - Api object for VolumeSnapshot -/// * `volume_snapshot_name` - Name of the VolumeSnapshot resource -/// -/// # Returns -/// -/// VolumeSnapshotStatus -pub async fn wait_untill_snapshot_is_ready( - vs_api: kube::Api, - vsc_api: kube::Api, - ebs_client: EbsClient, - volume_snapshot_name: &str, -) -> Result { - loop { - let snapshot = vs_api.get(volume_snapshot_name).await?; - if let Some(status) = snapshot.status { - if status.ready_to_use.unwrap_or(false) { - info!("Snapshot is ready: {:?}", status); - return Ok(status); - } - info!("Waiting for VolumeSnapshot to be ready..."); - - let vsc_name = status.bound_volume_snapshot_content_name.unwrap(); - match get_snapshot_handle(vsc_api.clone(), &vsc_name).await { - Ok(snapshot_handle) => { - let progress = - get_ebs_snapshot_progress(&ebs_client, snapshot_handle.clone()).await?; - info!( - "{}", - format!( - "Progress for EBS snapshot {} is: {}", - snapshot_handle, progress - ) - ); - } - Err(e) => { - error!("Failed to get snapshot handle: {}", e); - } - } - sleep(Duration::from_secs(5)).await; - } - } -} diff --git a/src/k8s_ops/vs/mod.rs b/src/k8s_ops/vs/mod.rs new file mode 100644 index 0000000..1754d95 --- /dev/null +++ b/src/k8s_ops/vs/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "backup")] +pub mod volume_snapshots; +pub mod volume_snapshots_operator; + +#[cfg(test)] +mod volume_snapshots_tests; diff --git a/src/k8s_ops/vs/volume_snapshots.rs b/src/k8s_ops/vs/volume_snapshots.rs new file mode 100644 index 0000000..2f6596f --- /dev/null +++ b/src/k8s_ops/vs/volume_snapshots.rs @@ -0,0 +1,61 @@ +use crate::aws_ops::ebs::get_ebs_snapshot_progress; +use crate::k8s_ops::vsc::volume_snapshot_contents::get_snapshot_handle; +use anyhow::Result; +use aws_sdk_ec2::Client as EbsClient; +use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::VolumeSnapshotContent; +use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshots::{ + VolumeSnapshot, VolumeSnapshotStatus, +}; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{info, warn}; + +/// Wait untill the VolumeSnapshot is ready +/// +/// # 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 +/// +/// VolumeSnapshotStatus +pub async fn wait_untill_snapshot_is_ready( + vs_api: &kube::Api, + vsc_api: &kube::Api, + ebs_client: &EbsClient, + volume_snapshot_name: &str, +) -> Result { + loop { + let snapshot = vs_api.get(volume_snapshot_name).await?; + if let Some(status) = snapshot.status { + if status.ready_to_use.unwrap_or(false) { + info!("Snapshot is ready: {:?}", status); + return Ok(status); + } + info!("Waiting for VolumeSnapshot to be ready..."); + + let vsc_name = status.bound_volume_snapshot_content_name.unwrap(); + match get_snapshot_handle(vsc_api.clone(), &vsc_name).await { + Ok(snapshot_handle) => { + let progress = + get_ebs_snapshot_progress(ebs_client.clone(), snapshot_handle.clone()) + .await?; + info!( + "{}", + format!( + "Progress for EBS snapshot {} regarding VS {} is: {}", + snapshot_handle, volume_snapshot_name, progress + ) + ); + } + Err(e) => { + warn!("Failed to get snapshot handle: {}", e); + } + } + sleep(Duration::from_secs(5)).await; + } + } +} diff --git a/src/k8s_ops/vs/volume_snapshots_operator.rs b/src/k8s_ops/vs/volume_snapshots_operator.rs new file mode 100644 index 0000000..4cac327 --- /dev/null +++ b/src/k8s_ops/vs/volume_snapshots_operator.rs @@ -0,0 +1,113 @@ +use std::collections::BTreeMap; + +use crate::k8s_ops::vsc::retain_policy::VSCRetainPolicy; +use kube::api::ObjectMeta; +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, + 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, + } + } + + /// 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, + vsc_retain_policy: VSCRetainPolicy, + ) -> VolumeSnapshot { + // 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("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("snap-kube/csi-volumesnapshot-restore-size".into(), size); + } + + // Create a base labels map + // Always add the namespace name + 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![VSResourceValues::Finalizers.get_value()]), + 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() + } + } +} diff --git a/src/k8s_ops/vs/volume_snapshots_tests.rs b/src/k8s_ops/vs/volume_snapshots_tests.rs new file mode 100644 index 0000000..5840ee4 --- /dev/null +++ b/src/k8s_ops/vs/volume_snapshots_tests.rs @@ -0,0 +1,91 @@ +#[cfg(test)] +mod tests { + use crate::k8s_ops::{ + vs::volume_snapshots_operator::VolumeSnapshotOperator, vsc::retain_policy::VSCRetainPolicy, + }; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn test_construct_volume_snapshot_resource() { + let vs_operator = VolumeSnapshotOperator::new( + "test-volume-snapshot".to_string(), + "default".to_string(), + "ebs.csi.aws.com".to_string(), + Some("test-pvc".to_string()), + Some("test-volume-snapshot-content".to_string()), + ); + let volume_snapshot = vs_operator.construct_volume_snapshot_resource( + Some("test-snapshot-handle".to_string()), + Some("1Gi".to_string()), + VSCRetainPolicy::Delete, + ); + assert_eq!( + volume_snapshot.metadata.name.unwrap(), + "test-volume-snapshot" + ); + assert_eq!(volume_snapshot.metadata.namespace.unwrap(), "default"); + assert_eq!( + volume_snapshot + .metadata + .annotations + .as_ref() + .unwrap() + .get("snap-kube/csi-driver-name"), + Some(&"ebs.csi.aws.com".to_string()) + ); + assert_eq!( + volume_snapshot + .metadata + .annotations + .as_ref() + .unwrap() + .get("snap-kube/csi-vsc-deletion-policy"), + Some(&"Delete".to_string()) + ); + assert_eq!( + volume_snapshot + .metadata + .annotations + .as_ref() + .unwrap() + .get("snap-kube/csi-volumesnapshot-handle"), + Some(&"test-snapshot-handle".to_string()) + ); + assert_eq!( + volume_snapshot + .metadata + .annotations + .unwrap() + .get("snap-kube/csi-volumesnapshot-restore-size"), + Some(&"1Gi".to_string()) + ); + assert_eq!( + volume_snapshot + .metadata + .labels + .unwrap() + .get("app.kubernetes.io/instance"), + Some(&"default".to_string()) + ); + assert_eq!( + volume_snapshot.spec.volume_snapshot_class_name.unwrap(), + "ebs.csi.aws.com" + ); + assert_eq!( + volume_snapshot + .spec + .source + .persistent_volume_claim_name + .unwrap(), + "test-pvc" + ); + assert_eq!( + volume_snapshot + .spec + .source + .volume_snapshot_content_name + .unwrap(), + "test-volume-snapshot-content" + ); + } +} diff --git a/src/k8s_ops/vsc/mod.rs b/src/k8s_ops/vsc/mod.rs new file mode 100644 index 0000000..e558ec7 --- /dev/null +++ b/src/k8s_ops/vsc/mod.rs @@ -0,0 +1,7 @@ +pub mod retain_policy; +pub mod volume_snapshot_contents; +#[cfg(feature = "restore")] +pub mod volume_snapshot_contents_operator; + +#[cfg(test)] +mod volume_snapshot_contents_tests; diff --git a/src/k8s_ops/vsc/retain_policy.rs b/src/k8s_ops/vsc/retain_policy.rs new file mode 100644 index 0000000..9e0cc40 --- /dev/null +++ b/src/k8s_ops/vsc/retain_policy.rs @@ -0,0 +1,53 @@ +use clap::ValueEnum; +use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::VolumeSnapshotContentDeletionPolicy; +use std::fmt::{self, Display, Formatter}; + +/// Represents the VolumeSnapshotContent Retain Policy +/// +/// It can be either Retain or Delete +#[derive(ValueEnum, Clone, Debug, Copy, PartialEq, Eq)] +pub enum VSCRetainPolicy { + Retain, + Delete, +} + +impl Display for VSCRetainPolicy { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + VSCRetainPolicy::Retain => write!(f, "Retain"), + VSCRetainPolicy::Delete => write!(f, "Delete"), + } + } +} + +impl From for VolumeSnapshotContentDeletionPolicy { + fn from(vsc_retain_policy: VSCRetainPolicy) -> Self { + match vsc_retain_policy { + VSCRetainPolicy::Retain => VolumeSnapshotContentDeletionPolicy::Retain, + VSCRetainPolicy::Delete => VolumeSnapshotContentDeletionPolicy::Delete, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vsc_retain_policy_display() { + assert_eq!(VSCRetainPolicy::Retain.to_string(), "Retain"); + assert_eq!(VSCRetainPolicy::Delete.to_string(), "Delete"); + } + + #[test] + fn test_vsc_retain_policy_into() { + assert_eq!( + VolumeSnapshotContentDeletionPolicy::from(VSCRetainPolicy::Retain), + VolumeSnapshotContentDeletionPolicy::Retain + ); + assert_eq!( + VolumeSnapshotContentDeletionPolicy::from(VSCRetainPolicy::Delete), + VolumeSnapshotContentDeletionPolicy::Delete + ); + } +} diff --git a/src/k8s_ops/vsc/volume_snapshot_contents.rs b/src/k8s_ops/vsc/volume_snapshot_contents.rs new file mode 100644 index 0000000..279eb5f --- /dev/null +++ b/src/k8s_ops/vsc/volume_snapshot_contents.rs @@ -0,0 +1,26 @@ +use anyhow::{bail, Result}; +use kube::Api; +use kube_custom_resources_rs::snapshot_storage_k8s_io::v1::volumesnapshotcontents::VolumeSnapshotContent; + +/// Get the snapshot handle from the VolumeSnapshotContent +/// +/// # Arguments +/// +/// * `vsc_api` - Api object for VolumeSnapshotContent +/// * `volume_snapshot_content_name` - Name of the VolumeSnapshotContent resource +/// +/// # Returns +/// +/// Snapshot handle +pub async fn get_snapshot_handle( + vsc_api: Api, + volume_snapshot_content_name: &str, +) -> Result { + let volume_snapshot_content = vsc_api.get(volume_snapshot_content_name).await?; + + if let Some(status) = volume_snapshot_content.status { + Ok(status.snapshot_handle.unwrap()) + } else { + bail!("Status of VolumeSnapshotContent is not available") + } +} diff --git a/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs b/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs new file mode 100644 index 0000000..b912d9b --- /dev/null +++ b/src/k8s_ops/vsc/volume_snapshot_contents_operator.rs @@ -0,0 +1,103 @@ +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, +}; + +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, + pub namespace: String, + pub volume_snapshot_name: String, + pub volume_snapshot_class: Option, + pub source_volume_handle: Option, + pub vsc_retain_policy: VSCRetainPolicy, +} + +impl VolumeSnapshotContentOperator { + pub fn new( + name: String, + namespace: String, + volume_snapshot_name: String, + volume_snapshot_class: Option, + source_volume_handle: Option, + vsc_retain_policy: VSCRetainPolicy, + ) -> Self { + Self { + name, + namespace, + volume_snapshot_name, + volume_snapshot_class, + source_volume_handle, + vsc_retain_policy, + } + } + + /// 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() + }, + spec: VolumeSnapshotContentSpec { + volume_snapshot_ref: VolumeSnapshotContentVolumeSnapshotRef { + 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(), + resource_version: Default::default(), + uid: Default::default(), + }, + deletion_policy: self.vsc_retain_policy.into(), + 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(VSCResourceValues::SourceVolumeMode.get_value()), + }, + 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/vsc/volume_snapshot_contents_tests.rs b/src/k8s_ops/vsc/volume_snapshot_contents_tests.rs new file mode 100644 index 0000000..b4984b6 --- /dev/null +++ b/src/k8s_ops/vsc/volume_snapshot_contents_tests.rs @@ -0,0 +1,61 @@ +#[cfg(test)] +mod tests { + use crate::k8s_ops::vsc::{ + retain_policy::VSCRetainPolicy, + volume_snapshot_contents_operator::VolumeSnapshotContentOperator, + }; + + #[tokio::test] + async fn test_construct_volume_snapshot_content_resource() { + let vsc_operator = VolumeSnapshotContentOperator::new( + "test-volume-snapshot-content".to_string(), + "default".to_string(), + "test-volume-snapshot".to_string(), + Some("ebs.csi.aws.com".to_string()), + Some("test-snapshot-handle".to_string()), + VSCRetainPolicy::Delete, + ); + let volume_snapshot_content = vsc_operator.construct_volume_snapshot_content_resource(); + assert_eq!( + volume_snapshot_content.metadata.name.unwrap(), + "test-volume-snapshot-content" + ); + assert_eq!( + volume_snapshot_content.metadata.namespace.unwrap(), + "default" + ); + assert_eq!( + volume_snapshot_content + .spec + .volume_snapshot_ref + .name + .unwrap(), + "test-volume-snapshot" + ); + assert_eq!( + volume_snapshot_content + .spec + .volume_snapshot_class_name + .unwrap(), + "ebs.csi.aws.com" + ); + assert_eq!( + volume_snapshot_content.spec.source.snapshot_handle.unwrap(), + "test-snapshot-handle" + ); + assert_eq!( + volume_snapshot_content.spec.source_volume_mode.unwrap(), + "Filesystem" + ); + assert_eq!( + volume_snapshot_content + .status + .as_ref() + .unwrap() + .snapshot_handle + .as_ref() + .unwrap(), + "test-snapshot-handle" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d6cb0a4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod aws_ops; +pub mod backup; +pub mod k8s_ops; +pub mod restore; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 1ee74b0..0000000 --- a/src/main.rs +++ /dev/null @@ -1,167 +0,0 @@ -mod aws_ops; -mod backup; -mod k8s_ops; -mod restore; -use anyhow::Result; -use backup::backup_operator::BackupOperator; -use clap::{Parser, Subcommand}; -use colored::Colorize; -use restore::restore_operator::RestoreOperator; -use tracing::info; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -#[command(propagate_version = true)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - Backup { - /// Region where the EBS volumes are stored - #[arg(long, required = false, default_value = "eu-west-1")] - region: String, - /// Source namespace - #[arg(long, required = true)] - source_ns: String, - /// VolumeSnapshotClass name - #[arg(long, required = true)] - volume_snapshot_class: String, - /// PVC name - #[arg(long, required = true)] - pvc_name: String, - /// VolumeSnapshot name - #[arg(long, required = true)] - volume_snapshot_name: String, - }, - Restore { - /// Source namespace - #[arg(long, required = true)] - source_ns: String, - /// Target namespace - #[arg(long, required = true)] - target_ns: String, - /// VolumeSnapshotClass name - #[arg(long, required = true)] - volume_snapshot_class: String, - /// PVC name - #[arg(long, required = true)] - pvc_name: String, - /// VolumeSnapshot name - #[arg(long, required = true)] - volume_snapshot_name: String, - /// Target VolumeSnapshotContent name - #[arg(long, required = true)] - target_snapshot_content_name: String, - /// StorageClass name - #[arg(long, required = true)] - storage_class_name: String, - }, - Full { - /// Region where the EBS volumes are stored - #[arg(long, required = false, default_value = "eu-west-1")] - region: String, - /// Source namespace - #[arg(long, required = true)] - source_ns: String, - /// Target namespace - #[arg(long, required = true)] - target_ns: String, - /// VolumeSnapshotClass name - #[arg(long, required = true)] - volume_snapshot_class: String, - /// PVC name - #[arg(long, required = true)] - pvc_name: String, - /// VolumeSnapshot name - #[arg(long, required = true)] - volume_snapshot_name: String, - /// Target VolumeSnapshotContent name - #[arg(long, required = true)] - target_snapshot_content_name: String, - /// StorageClass name - #[arg(long, required = true)] - storage_class_name: String, - }, -} - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt::init(); - - let cli = Cli::parse(); - match cli.command { - Commands::Backup { - region, - source_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - } => { - let backup_operator = BackupOperator::new( - region, - source_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - ); - info!("{}", "Starting Backup process...".bold().blue()); - backup_operator.backup().await?; - } - Commands::Restore { - source_ns, - target_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - target_snapshot_content_name, - storage_class_name, - } => { - let restore_operator = RestoreOperator::new( - source_ns, - target_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - target_snapshot_content_name, - storage_class_name, - ); - info!("{}", "Starting Restore process...".bold().blue()); - restore_operator.restore().await?; - } - Commands::Full { - region, - source_ns, - target_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - target_snapshot_content_name, - storage_class_name, - } => { - let backup_operator = BackupOperator::new( - region.clone(), - source_ns.clone(), - volume_snapshot_class.clone(), - pvc_name.clone(), - volume_snapshot_name.clone(), - ); - let restore_operator = RestoreOperator::new( - source_ns, - target_ns, - volume_snapshot_class, - pvc_name, - volume_snapshot_name, - target_snapshot_content_name, - storage_class_name, - ); - info!("{}", "Starting Backup process...".bold().blue()); - backup_operator.backup().await?; - info!("{}", "Starting Restore process...".bold().blue()); - restore_operator.restore().await?; - } - }; - Ok(()) -} diff --git a/src/restore/mod.rs b/src/restore/mod.rs index a855bf5..5bf367a 100644 --- a/src/restore/mod.rs +++ b/src/restore/mod.rs @@ -1 +1,4 @@ +#[cfg(feature = "restore")] pub mod restore_operator; +#[cfg(feature = "restore")] +pub mod restore_payload; diff --git a/src/restore/restore_operator.rs b/src/restore/restore_operator.rs index 7db78fd..ab1d2cd 100644 --- a/src/restore/restore_operator.rs +++ b/src/restore/restore_operator.rs @@ -1,10 +1,16 @@ 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, + pvc::{ + 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, + 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, @@ -12,200 +18,151 @@ 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, -} +use super::restore_payload::RestorePayload; /// 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, -} +pub struct RestoreOperator; 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 - /// - /// # Returns - /// - /// RestoreOperator instance - /// - /// # 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(), - /// ); - /// ``` - pub fn new( - 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, - ) -> Self { - RestoreOperator { - source_ns, - target_ns, - volume_snapshot_class, - pvc_name, - 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<()> { + /// Restores one or more PVCs from a VolumeSnapshot to a specific namespace + pub async fn restore(restore_payload: RestorePayload) -> 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), + let restore_k8s_apis_struct = RestoreKubernetesApisStruct { + source_vs_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: KubePvcApi { + api: Api::namespaced(k8s_client.clone(), restore_payload.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 - .source_vs_api - .get(&self.volume_snapshot_name) - .await - { - Ok(snapshot) => snapshot.status.unwrap(), - Err(e) => { - bail!("Failed to get VolumeSnapshot status: {}", e) - } + // Check if we will restore all PVCs in the namespace + let pvcs = if restore_payload.include_all() { + get_pvcs_available(&restore_k8s_apis_struct.source_pvcs_api).await? + } else { + vec![restore_payload.pvc_name().to_string()] }; - let bound_vsc_name = status.bound_volume_snapshot_content_name.unwrap(); - let restore_size = status.restore_size.unwrap(); - - let snapshot_handle = get_snapshot_handle( - target_kubernetes_apis_struct.vsc_api.clone(), - &bound_vsc_name, - ) - .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 pp = PostParams::default(); - match target_kubernetes_apis_struct - .vsc_api - .create(&pp, &snapshot_content) - .await - { - Ok(snapshot_content) => info!("Created VolumeSnapshotContent: {:?}", snapshot_content), - Err(e) => panic!("Failed to create VolumeSnapshotContent: {}", e), - } + // We will iterate over the PVCs vector and restore each PVC + + for pvc in pvcs { + info!("Restoring PVC: {}", pvc); + let volume_snapshot_name = format!("{}-{}", restore_payload.vs_name_prefix(), pvc); + let volume_snapshot_content_name = + format!("{}-{}", restore_payload.vsc_name_prefix(), pvc); + + // Check if the PVC exists in the target namespace, it should not exist + check_if_pvc_exists(&restore_k8s_apis_struct.target_pvcs_api, &pvc, false).await?; + + let status: VolumeSnapshotStatus = match restore_k8s_apis_struct + .source_vs_api + .get(&volume_snapshot_name) + .await + { + Ok(snapshot) => snapshot.status.unwrap(), + Err(e) => { + bail!("Failed to get VolumeSnapshot status: {}", e) + } + }; + + let bound_vsc_name = status.bound_volume_snapshot_content_name.unwrap(); + let restore_size = status.restore_size.unwrap(); + + let snapshot_handle = + get_snapshot_handle(restore_k8s_apis_struct.vsc_api.clone(), &bound_vsc_name) + .await?; - // 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(), - None, - Some(self.target_snapshot_content_name.to_string()), - 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 { - Ok(target_volume_snapshot) => { - info!("Created VolumeSnapshot: {:?}", target_volume_snapshot) + let vsc_operator = VolumeSnapshotContentOperator::new( + volume_snapshot_content_name.clone(), + restore_payload.target_ns().to_string(), + volume_snapshot_name.clone(), + Some(restore_payload.volume_snapshot_class().to_string()), + Some(snapshot_handle.clone()), + *restore_payload.vsc_retain_policy(), + ); + + let snapshot_content = vsc_operator.construct_volume_snapshot_content_resource(); + + let pp = PostParams::default(); + match restore_k8s_apis_struct + .vsc_api + .create(&pp, &snapshot_content) + .await + { + Ok(snapshot_content) => { + info!("Created VolumeSnapshotContent: {:?}", snapshot_content) + } + Err(e) => panic!("Failed to create VolumeSnapshotContent: {}", e), } - 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()), - None, - self.volume_snapshot_name.to_string(), - restore_size.to_string(), - ); - - info!("Restoring PVC..."); - let pp = PostParams::default(); - match target_kubernetes_apis_struct - .pvcs_api - .create(&pp, &pvc) - .await - { - Ok(pvc) => info!("Restored PVC: {:?}", pvc), - Err(e) => panic!("Failed to restore PVC: {}", e), + let vs_operator = VolumeSnapshotOperator::new( + volume_snapshot_name.clone(), + restore_payload.target_ns().to_string(), + restore_payload.volume_snapshot_class().to_string(), + None, + Some(volume_snapshot_content_name), + ); + + let target_volume_snapshot = vs_operator.construct_volume_snapshot_resource( + Some(snapshot_handle.to_string()), + Some(restore_size.to_string()), + *restore_payload.vsc_retain_policy(), + ); + + info!("Creating VolumeSnapshot in the target namespace..."); + let pp = PostParams::default(); + match restore_k8s_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), + } + + // Restore the PVC for each pvc available + let pvc_payload = PVCOperatorPayload::new( + pvc, + restore_payload.target_ns(), + Some(restore_payload.storage_class_name().to_string()), + None, + volume_snapshot_name, + 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 restore_k8s_apis_struct + .target_pvcs_api + .api + .create(&pp, &pvc) + .await + { + Ok(pvc) => info!("Restored PVC: {:?}", pvc), + Err(e) => panic!("Failed to restore PVC: {}", e), + } } Ok(()) } } + +/// A struct for holding the Kubernetes APIs for the restore operation +struct RestoreKubernetesApisStruct { + source_vs_api: Api, + source_pvcs_api: KubePvcApi, + target_vs_api: Api, + target_pvcs_api: KubePvcApi, + vsc_api: Api, +} diff --git a/src/restore/restore_payload.rs b/src/restore/restore_payload.rs new file mode 100644 index 0000000..3959d2b --- /dev/null +++ b/src/restore/restore_payload.rs @@ -0,0 +1,76 @@ +use crate::k8s_ops::vsc::retain_policy::VSCRetainPolicy; + +pub struct RestorePayload { + pub source_ns: String, + pub target_ns: String, + pub volume_snapshot_class: String, + pub pvc_name: String, + pub include_all: bool, + pub vs_name_prefix: String, + pub vsc_name_prefix: String, + pub storage_class_name: String, + pub vsc_retain_policy: VSCRetainPolicy, +} + +impl RestorePayload { + #[allow(clippy::too_many_arguments)] + pub fn new( + source_ns: impl Into, + target_ns: impl Into, + volume_snapshot_class: impl Into, + pvc_name: impl Into, + include_all: bool, + vs_name_prefix: impl Into, + vsc_name_prefix: impl Into, + storage_class_name: impl Into, + vsc_retain_policy: VSCRetainPolicy, + ) -> Self { + Self { + source_ns: source_ns.into(), + target_ns: target_ns.into(), + volume_snapshot_class: volume_snapshot_class.into(), + pvc_name: pvc_name.into(), + include_all, + vs_name_prefix: vs_name_prefix.into(), + vsc_name_prefix: vsc_name_prefix.into(), + storage_class_name: storage_class_name.into(), + vsc_retain_policy, + } + } + + pub fn source_ns(&self) -> &str { + &self.source_ns + } + + pub fn target_ns(&self) -> &str { + &self.target_ns + } + + pub fn volume_snapshot_class(&self) -> &str { + &self.volume_snapshot_class + } + + pub fn pvc_name(&self) -> &str { + &self.pvc_name + } + + pub fn include_all(&self) -> bool { + self.include_all + } + + pub fn vs_name_prefix(&self) -> &str { + &self.vs_name_prefix + } + + pub fn vsc_name_prefix(&self) -> &str { + &self.vsc_name_prefix + } + + pub fn storage_class_name(&self) -> &str { + &self.storage_class_name + } + + pub fn vsc_retain_policy(&self) -> &VSCRetainPolicy { + &self.vsc_retain_policy + } +}