From 34a34422a2774efc32ba461d6496dee8971d59ff Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 21 Oct 2024 16:42:48 -0400 Subject: [PATCH] `conductor`: Add support for Azure Backup and Restore (#980) Signed-off-by: Ian Stanton --- conductor/Cargo.lock | 522 ++++++++++++++++++++++++++-- conductor/Cargo.toml | 6 + conductor/src/azure/azure_error.rs | 12 + conductor/src/azure/mod.rs | 2 + conductor/src/azure/uami_builder.rs | 316 +++++++++++++++++ conductor/src/cloud.rs | 12 + conductor/src/errors.rs | 5 + conductor/src/lib.rs | 76 +++- conductor/src/main.rs | 151 +++++++- 9 files changed, 1068 insertions(+), 34 deletions(-) create mode 100644 conductor/src/azure/azure_error.rs create mode 100644 conductor/src/azure/mod.rs create mode 100644 conductor/src/azure/uami_builder.rs diff --git a/conductor/Cargo.lock b/conductor/Cargo.lock index 556ffb787..b8713f0a1 100644 --- a/conductor/Cargo.lock +++ b/conductor/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tokio", @@ -223,7 +223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -280,6 +280,96 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.1", + "futures-lite 2.3.0", + "rustix", + "tracing", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -302,6 +392,12 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.82" @@ -654,15 +750,111 @@ dependencies = [ "tracing", ] +[[package]] +name = "azure_core" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b552ad43a45a746461ec3d3a51dfb6466b4759209414b439c165eb6a6b7729e" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "dyn-clone", + "futures", + "getrandom 0.2.15", + "http-types", + "once_cell", + "paste", + "pin-project", + "rand 0.8.5", + "reqwest 0.12.7", + "rustc_version", + "serde", + "serde_json", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "azure_identity" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ddd80344317c40c04b603807b63a5cefa532f1b43522e72f480a988141f744" +dependencies = [ + "async-lock", + "async-process", + "async-trait", + "azure_core", + "futures", + "oauth2", + "pin-project", + "serde", + "time", + "tracing", + "tz-rs", + "url", + "uuid", +] + +[[package]] +name = "azure_mgmt_authorization" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb60abdc00edf3545c0a235fbd3aa26a8dc870676361c4064114de0c4c607b8" +dependencies = [ + "azure_core", + "bytes", + "futures", + "log", + "once_cell", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "azure_mgmt_msi" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85adbe25b00edbbdd4034bc917f73d3eb647d5f6872185f6e1dcdf13950c91e" +dependencies = [ + "azure_core", + "bytes", + "futures", + "log", + "once_cell", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "azure_mgmt_storage" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9943c26303482bb57afc0ab092d0638e7813ae30a0bb055dca52bf4952d6e4d" +dependencies = [ + "azure_core", + "bytes", + "futures", + "log", + "once_cell", + "serde", + "serde_json", + "time", +] + [[package]] name = "backoff" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ - "getrandom", + "getrandom 0.2.15", "instant", - "rand", + "rand 0.8.5", ] [[package]] @@ -744,6 +936,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + [[package]] name = "brotli" version = "6.0.0" @@ -834,6 +1039,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "conductor" version = "0.1.0" @@ -843,6 +1057,11 @@ dependencies = [ "anyhow", "aws-config", "aws-sdk-cloudformation", + "azure_core", + "azure_identity", + "azure_mgmt_authorization", + "azure_mgmt_msi", + "azure_mgmt_storage", "base64 0.21.7", "chrono", "controller", @@ -855,7 +1074,7 @@ dependencies = [ "opentelemetry 0.18.0", "opentelemetry-prometheus", "pgmq", - "rand", + "rand 0.8.5", "reqwest 0.12.7", "schemars", "serde", @@ -864,6 +1083,7 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "uuid", ] [[package]] @@ -872,6 +1092,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" + [[package]] name = "controller" version = "0.50.2" @@ -888,7 +1114,7 @@ dependencies = [ "opentelemetry 0.19.0", "passwords", "prometheus", - "rand", + "rand 0.8.5", "regex", "reqwest 0.11.27", "schemars", @@ -1225,6 +1451,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1350,6 +1597,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -1401,6 +1676,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1410,7 +1696,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1607,7 +1893,7 @@ dependencies = [ "idna 0.4.0", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "thiserror", "tinyvec", "tokio", @@ -1628,7 +1914,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -1736,6 +2022,26 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel 1.9.0", + "base64 0.13.1", + "futures-lite 1.13.0", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.9.4" @@ -1985,6 +2291,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "instant" version = "0.1.13" @@ -2147,7 +2459,7 @@ dependencies = [ "openssl", "pem 1.1.1", "pin-project", - "rand", + "rand 0.8.5", "secrecy", "serde", "serde_json", @@ -2399,7 +2711,7 @@ dependencies = [ "hermit-abi 0.3.9", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2462,7 +2774,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2503,6 +2815,34 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom 0.2.15", + "http 0.2.12", + "rand 0.8.5", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.36.4" @@ -2650,7 +2990,7 @@ dependencies = [ "once_cell", "opentelemetry_api 0.18.0", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", @@ -2672,7 +3012,7 @@ dependencies = [ "once_cell", "opentelemetry_api 0.19.0", "percent-encoding", - "rand", + "rand 0.8.5", "thiserror", "tokio", "tokio-stream", @@ -2699,6 +3039,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2819,6 +3165,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.1", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -2846,6 +3203,21 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2936,6 +3308,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2943,8 +3328,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2954,7 +3349,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2963,7 +3367,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2973,7 +3386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3da5cbb4c27c5150c03a54a7e4745437cd90f9e329ae657c0b889a144bb7be" dependencies = [ "proc-macro-hack", - "rand", + "rand 0.8.5", "random-number-macro-impl", ] @@ -3012,7 +3425,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -3202,7 +3615,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -3222,7 +3635,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -3485,6 +3898,27 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3563,7 +3997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3665,7 +4099,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -3761,7 +4195,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -3801,7 +4235,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", @@ -4021,7 +4455,10 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "js-sys", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -4347,7 +4784,7 @@ dependencies = [ "http 0.2.12", "httparse", "log", - "rand", + "rand 0.8.5", "sha1", "thiserror", "url", @@ -4360,6 +4797,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "tz-rs" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" +dependencies = [ + "const_fn", +] + [[package]] name = "unicase" version = "2.7.0" @@ -4435,6 +4881,7 @@ dependencies = [ "form_urlencoded", "idna 0.5.0", "percent-encoding", + "serde", ] [[package]] @@ -4473,6 +4920,15 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4497,6 +4953,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "want" version = "0.3.1" @@ -4506,6 +4968,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/conductor/Cargo.toml b/conductor/Cargo.toml index eeef1e9d8..2579bb345 100644 --- a/conductor/Cargo.toml +++ b/conductor/Cargo.toml @@ -36,6 +36,12 @@ anyhow = "1.0.82" serde_yaml = "0.9.34" reqwest = { version = "0.12.3", features = ["json"] } google-cloud-storage = "0.22.1" +azure_identity = "0.21.0" +azure_mgmt_msi = "0.21.0" +azure_mgmt_authorization = "0.21.0" +azure_mgmt_storage = "0.21.0" +azure_core = "0.21.0" +uuid = "1.10.0" [dependencies.kube] features = ["runtime", "client", "derive"] diff --git a/conductor/src/azure/azure_error.rs b/conductor/src/azure/azure_error.rs new file mode 100644 index 000000000..b5b96b513 --- /dev/null +++ b/conductor/src/azure/azure_error.rs @@ -0,0 +1,12 @@ +use azure_core::Error as AzureSDKError; +use reqwest::Error as ReqwestError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AzureError { + #[error("Error with Azure SDK {0}")] + AzureSDKError(#[from] AzureSDKError), + + #[error("Error with Azure REST API {0}")] + AzureRestAPIError(#[from] ReqwestError), +} diff --git a/conductor/src/azure/mod.rs b/conductor/src/azure/mod.rs new file mode 100644 index 000000000..db350f0ea --- /dev/null +++ b/conductor/src/azure/mod.rs @@ -0,0 +1,2 @@ +pub mod azure_error; +pub mod uami_builder; diff --git a/conductor/src/azure/uami_builder.rs b/conductor/src/azure/uami_builder.rs new file mode 100644 index 000000000..801f931ab --- /dev/null +++ b/conductor/src/azure/uami_builder.rs @@ -0,0 +1,316 @@ +use crate::azure::azure_error; +use azure_core::auth::TokenCredential; +use azure_core::error::Error as AzureSDKError; +use azure_error::AzureError; +use azure_identity::TokenCredentialOptions; +use azure_identity::WorkloadIdentityCredential; +use azure_mgmt_authorization; +use azure_mgmt_authorization::models::{RoleAssignment, RoleAssignmentProperties}; +use azure_mgmt_msi::models::{ + FederatedIdentityCredential, FederatedIdentityCredentialProperties, Identity, TrackedResource, +}; +use futures::StreamExt; +use log::info; +use std::sync::Arc; + +// Get credentials from workload identity +pub async fn get_credentials() -> Result, AzureError> { + let options: TokenCredentialOptions = Default::default(); + let credential = WorkloadIdentityCredential::create(options)?; + Ok(Arc::new(credential)) +} + +// Create User Assigned Managed Identity +pub async fn create_uami( + resource_group_prefix: &str, + subscription_id: &str, + uami_name: &str, + region: &str, + credentials: Arc, +) -> Result { + let resource_group = format!("{resource_group_prefix}-storage-rg"); + let msi_client = azure_mgmt_msi::Client::builder(credentials).build()?; + + // Set parameters for User Assigned Managed Identity + let uami_params = Identity { + tracked_resource: TrackedResource { + resource: Default::default(), + tags: None, + location: region.to_string(), + }, + properties: None, + }; + + // Create User Assigned Managed Identity + let uami_created = msi_client + .user_assigned_identities_client() + .create_or_update(subscription_id, resource_group, uami_name, uami_params) + .await?; + info!("Created UAMI for {uami_name}"); + Ok(uami_created) +} + +// Get role definition ID +pub async fn get_role_definition_id( + subscription_id: &str, + role_name: &str, + credentials: Arc, +) -> Result { + let role_definition_client = azure_mgmt_authorization::Client::builder(credentials).build()?; + let scope = format!("/subscriptions/{subscription_id}"); + // Get role definition for role name + let role_definition = role_definition_client.role_definitions_client().list(scope); + let mut role_definition_stream = role_definition.into_stream(); + while let Some(role_definition_page) = role_definition_stream.next().await { + let role_definition_page = role_definition_page?; + for item in role_definition_page.value { + if item.properties.unwrap().role_name == Some(role_name.to_string()) { + return Ok(item.id.unwrap()); + } + } + } + // Return error if not found + Err(AzureError::from(AzureSDKError::new( + azure_core::error::ErrorKind::Other, + format!("Role definition {} not found", role_name), + ))) +} + +// Get storage account ID +pub async fn get_storage_account_id( + subscription_id: &str, + resource_group_prefix: &str, + storage_account_name: &str, + credentials: Arc, +) -> Result { + let resource_group = format!("{resource_group_prefix}-storage-rg"); + let storage_client = azure_mgmt_storage::Client::builder(credentials).build()?; + let storage_account_list = storage_client + .storage_accounts_client() + .list_by_resource_group(resource_group, subscription_id); + let mut storage_account_stream = storage_account_list.into_stream(); + let mut storage_account = None; + while let Some(storage_account_page) = storage_account_stream.next().await { + let storage_account_page = storage_account_page?; + for item in storage_account_page.value { + if item.tracked_resource.resource.name == Some(storage_account_name.to_string()) { + storage_account = Some(item); + break; + } + } + if storage_account.is_some() { + break; + } + } + Ok(storage_account + .unwrap() + .tracked_resource + .resource + .id + .unwrap()) +} + +// Check if role assignment exists +pub async fn role_assignment_exists( + subscription_id: &str, + _storage_account_id: &str, + uami_id: &str, + credentials: Arc, +) -> Result { + let role_assignment_client = + azure_mgmt_authorization::Client::builder(credentials.clone()).build()?; + + let role_definition = get_role_definition_id( + subscription_id, + "Storage Blob Data Contributor", + credentials.clone(), + ) + .await?; + + let role_assignment_list = role_assignment_client + .role_assignments_client() + .list_for_subscription(subscription_id); + let mut role_assignment_stream = role_assignment_list.into_stream(); + while let Some(role_assignment_page) = role_assignment_stream.next().await { + let role_assignment_page = role_assignment_page?; + for item in role_assignment_page.value { + if item.properties.clone().unwrap().role_definition_id == role_definition + && item.properties.unwrap().principal_id == uami_id + { + return Ok(true); + } + } + } + Ok(false) +} + +// Create Role Assignment for UAMI +pub async fn create_role_assignment( + subscription_id: &str, + resource_group_prefix: &str, + storage_account_name: &str, + namespace: &str, + uami_principal_id: &str, + credentials: Arc, +) -> Result<(), AzureError> { + let resource_group = format!("{resource_group_prefix}-storage-rg"); + let role_assignment_name = uuid::Uuid::new_v4().to_string(); + let role_assignment_client = + azure_mgmt_authorization::Client::builder(credentials.clone()).build()?; + + let role_definition = get_role_definition_id( + subscription_id, + "Storage Blob Data Contributor", + credentials.clone(), + ) + .await?; + + // TODO(ianstanton) Set conditions for Role Assignment. These should allow for read / write + // to the instance's directory in the blob + + let storage_account_id = get_storage_account_id( + subscription_id, + &resource_group, + storage_account_name, + credentials.clone(), + ) + .await?; + + // Check if role assignment already exists + info!("Checking if role assignment exists"); + if role_assignment_exists( + subscription_id, + &storage_account_id, + uami_principal_id, + credentials, + ) + .await? + { + info!("Role assignment already exists, skipping creation"); + return Ok(()); + } + + // Set parameters for Role Assignment + let role_assignment_params = azure_mgmt_authorization::models::RoleAssignmentCreateParameters { + properties: RoleAssignmentProperties { + scope: None, + role_definition_id: role_definition, + principal_id: uami_principal_id.to_string(), + principal_type: None, + description: None, + condition: None, + condition_version: None, + created_on: None, + updated_on: None, + created_by: None, + updated_by: None, + delegated_managed_identity_resource_id: None, + }, + }; + + // Create Role Assignment. Scope should be storage account ID + role_assignment_client + .role_assignments_client() + .create( + storage_account_id, + role_assignment_name, + role_assignment_params, + ) + .await?; + info!("Created Role Assignment for {namespace}"); + Ok(()) +} + +// Get OIDC Issuer URL from AKS cluster using rest API. This is necessary because the azure_mgmt_containerservice +// crate is no longer being built: https://github.com/Azure/azure-sdk-for-rust/pull/1243 +pub async fn get_cluster_issuer( + subscription_id: &str, + resource_group_prefix: &str, + cluster_name: &str, + credentials: Arc, +) -> Result { + let resource_group = format!("{resource_group_prefix}-aks-rg"); + let client = reqwest::Client::new(); + let url = format!( + "https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.ContainerService/managedClusters/{cluster_name}?api-version=2024-08-01"); + let scopes: &[&str] = &["https://management.azure.com/.default"]; + + let response = client + .get(&url) + .header( + "Authorization", + format!( + "Bearer {}", + credentials.get_token(scopes).await?.token.secret() + ), + ) + .send() + .await?; + + let response_json = response.json::().await?; + let issuer_url = response_json["properties"]["oidcIssuerProfile"]["issuerURL"] + .as_str() + .unwrap(); + Ok(issuer_url.to_string()) +} + +// Create Federated Identity Credentials for the UAMI +pub async fn create_federated_identity_credentials( + subscription_id: &str, + resource_group_prefix: &str, + instance_name: &str, + credentials: Arc, +) -> Result<(), AzureError> { + let resource_group = format!("{resource_group_prefix}-storage-rg"); + let uami_name = instance_name; + let federated_identity_client = azure_mgmt_msi::Client::builder(credentials.clone()).build()?; + let cluster_issuer = get_cluster_issuer( + subscription_id, + &resource_group, + &format!("aks-{resource_group_prefix}-aks-data-1"), + credentials.clone(), + ) + .await?; + + // Set parameters for Federated Identity Credentials + let federated_identity_params = FederatedIdentityCredential { + proxy_resource: Default::default(), + properties: Some(FederatedIdentityCredentialProperties { + issuer: cluster_issuer, + subject: format!("system:serviceaccount:{instance_name}:{instance_name}"), + audiences: vec!["api://AzureADTokenExchange".to_string()], + }), + }; + + // Create Federated Identity Credentials + federated_identity_client + .federated_identity_credentials_client() + .create_or_update( + subscription_id, + resource_group, + uami_name, + instance_name, + federated_identity_params, + ) + .await?; + info!("Created Federated Credential for {instance_name}"); + Ok(()) +} + +// Delete User Assigned Managed Identity +pub async fn delete_uami( + subscription_id: &str, + resource_group_prefix: &str, + uami_name: &str, + credentials: Arc, +) -> Result<(), AzureError> { + let resource_group = format!("{resource_group_prefix}-storage-rg"); + let msi_client = azure_mgmt_msi::Client::builder(credentials).build()?; + msi_client + .user_assigned_identities_client() + .delete(subscription_id, resource_group, uami_name) + .send() + .await?; + info!("Deleted UAMI for {uami_name}"); + Ok(()) +} diff --git a/conductor/src/cloud.rs b/conductor/src/cloud.rs index c48c5eee3..97104ffaa 100644 --- a/conductor/src/cloud.rs +++ b/conductor/src/cloud.rs @@ -2,6 +2,7 @@ pub struct CloudProviderBuilder { gcp: bool, aws: bool, + azure: bool, } impl CloudProviderBuilder { @@ -9,6 +10,7 @@ impl CloudProviderBuilder { CloudProviderBuilder { gcp: false, aws: false, + azure: false, } } @@ -22,11 +24,18 @@ impl CloudProviderBuilder { self } + pub fn azure(mut self, value: bool) -> Self { + self.azure = value; + self + } + pub fn build(self) -> CloudProvider { if self.gcp { CloudProvider::GCP } else if self.aws { CloudProvider::AWS + } else if self.azure { + CloudProvider::Azure } else { CloudProvider::Unknown } @@ -35,6 +44,7 @@ impl CloudProviderBuilder { pub enum CloudProvider { AWS, + Azure, GCP, Unknown, } @@ -43,6 +53,7 @@ impl CloudProvider { pub fn as_str(&self) -> &'static str { match self { CloudProvider::AWS => "aws", + CloudProvider::Azure => "azure", CloudProvider::GCP => "gcp", CloudProvider::Unknown => "unknown", } @@ -51,6 +62,7 @@ impl CloudProvider { pub fn prefix(&self) -> &'static str { match self { CloudProvider::AWS => "s3://", + CloudProvider::Azure => "https://", CloudProvider::GCP => "gs://", CloudProvider::Unknown => "", } diff --git a/conductor/src/errors.rs b/conductor/src/errors.rs index 9c5513c91..23fb7a9d3 100644 --- a/conductor/src/errors.rs +++ b/conductor/src/errors.rs @@ -1,4 +1,6 @@ +use crate::azure; use aws_sdk_cloudformation::Error as CFError; +use azure::azure_error::AzureError; use google_cloud_storage::http::Error as GcsError; use kube; use pgmq::errors::PgmqError; @@ -57,4 +59,7 @@ pub enum ConductorError { /// Dataplane error #[error("Dataplane not found error: {0}")] DataplaneError(String), + + #[error("Error with Azure SDK {0}")] + AzureError(#[from] AzureError), } diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index 8a8dca781..0faa7fd2c 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -1,4 +1,5 @@ pub mod aws; +pub mod azure; pub mod cloud; pub mod errors; pub mod extensions; @@ -20,6 +21,11 @@ use k8s_openapi::api::core::v1::{Namespace, Secret}; use kube::api::{DeleteParams, ListParams, Patch, PatchParams}; +use crate::azure::uami_builder::{ + create_federated_identity_credentials, create_role_assignment, create_uami, delete_uami, + get_credentials, +}; + use chrono::{DateTime, SecondsFormat, Utc}; use kube::{Api, Client, ResourceExt}; use log::{debug, info, warn}; @@ -45,7 +51,7 @@ pub async fn generate_spec( let mut spec = spec.clone(); match cloud_provider { - CloudProvider::AWS | CloudProvider::GCP => { + CloudProvider::AWS | CloudProvider::GCP | CloudProvider::Azure => { let prefix = cloud_provider.prefix(); // Format the backups_path with the correct prefix @@ -586,6 +592,74 @@ pub async fn delete_gcp_storage_workload_identity_binding( Ok(()) } +pub async fn create_azure_storage_workload_identity_binding( + azure_subscription_id: &str, + azure_resource_group_prefix: &str, + azure_region: &str, + azure_storage_account: &str, + namespace: &str, +) -> Result { + let credentials = get_credentials().await?; + + // Create UAMI + let uami = create_uami( + azure_resource_group_prefix, + azure_subscription_id, + namespace, + azure_region, + credentials.clone(), + ) + .await?; + + // Get UAMI Client ID to return and pass to ServiceAccountTemplate + let uami_client_id = uami.properties.clone().unwrap().client_id.unwrap(); + + // Create Role Assignment for UAMI + let uami_principal_id = uami.properties.unwrap().principal_id.unwrap(); + create_role_assignment( + azure_subscription_id, + azure_resource_group_prefix, + azure_storage_account, + &namespace, + &uami_principal_id, + credentials.clone(), + ) + .await?; + + // Create Federated Credential for the UAMI + create_federated_identity_credentials( + azure_subscription_id, + azure_resource_group_prefix, + namespace, + credentials.clone(), + ) + .await?; + + Ok(uami_client_id) +} + +// TODO(ianstanton) Check to see whether we need to delete the role assignment and federated +// credentials +pub async fn delete_azure_storage_workload_identity_binding( + azure_subscription_id: &str, + azure_resource_group: &str, + namespace: &str, +) -> Result<(), ConductorError> { + let credentials = get_credentials().await?; + + // Delete UAMI + delete_uami( + azure_subscription_id, + azure_resource_group, + namespace, + credentials.clone(), + ) + .await?; + info!("Deleted UAMI"); + + Ok(()) +} + #[cfg(test)] mod tests { const DECODER: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( diff --git a/conductor/src/main.rs b/conductor/src/main.rs index b4af1ed5b..984168613 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -3,8 +3,9 @@ use actix_web_opentelemetry::{PrometheusMetricsHandler, RequestTracing}; use conductor::errors::ConductorError; use conductor::monitoring::CustomMetrics; use conductor::{ - cloud::CloudProvider, create_cloudformation, create_gcp_storage_workload_identity_binding, - create_namespace, create_or_update, delete, delete_cloudformation, + cloud::CloudProvider, create_azure_storage_workload_identity_binding, create_cloudformation, + create_gcp_storage_workload_identity_binding, create_namespace, create_or_update, delete, + delete_azure_storage_workload_identity_binding, delete_cloudformation, delete_gcp_storage_workload_identity_binding, delete_namespace, generate_cron_expression, generate_spec, get_coredb_error_without_status, get_one, get_pg_conn, lookup_role_arn, restart_coredb, types, @@ -14,7 +15,8 @@ use crate::metrics_reporter::run_metrics_reporter; use crate::status_reporter::run_status_reporter; use conductor::routes::health::background_threads_running; use controller::apis::coredb_types::{ - Backup, CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot, + AzureCredentials, Backup, CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, + VolumeSnapshot, }; use controller::apis::postgres_parameters::{ConfigValue, PgConfig}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; @@ -85,6 +87,27 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { .unwrap_or_else(|_| "".to_owned()) .parse() .expect("error parsing GCP_PROJECT_NUMBER"); + let is_azure: bool = env::var("IS_AZURE") + .unwrap_or_else(|_| "false".to_owned()) + .parse() + .expect("error parsing IS_AZURE"); + let azure_storage_account: String = env::var("AZURE_STORAGE_ACCOUNT") + .unwrap_or_else(|_| "".to_owned()) + .parse() + .expect("error parsing AZURE_STORAGE_ACCOUNT"); + let azure_subscription_id: String = env::var("AZURE_SUBSCRIPTION_ID") + .unwrap_or_else(|_| "".to_owned()) + .parse() + .expect("error parsing AZURE_SUBSCRIPTION_ID"); + // This is necessary for working with multiple resource groups. Example format: cdb-plat-eus-dev + let azure_resource_group_prefix: String = env::var("AZURE_RESOURCE_GROUP_PREFIX ") + .unwrap_or_else(|_| "".to_owned()) + .parse() + .expect("error parsing AZURE_RESOURCE_GROUP_PREFIX"); + let azure_region: String = env::var("AZURE_REGION") + .unwrap_or_else(|_| "".to_owned()) + .parse() + .expect("error parsing AZURE_REGION"); let is_loadbalancer_public: bool = env::var("IS_LOADBALANCER_PUBLIC") .unwrap_or_else(|_| "true".to_owned()) .parse() @@ -95,9 +118,13 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { panic!("CF_TEMPLATE_BUCKET is required when IS_CLOUD_FORMATION is true"); } - // Error and exit if both IS_CLOUD_FORMATION and IS_GCP are set to true - if is_cloud_formation && is_gcp { - panic!("Cannot have both IS_CLOUD_FORMATION and IS_GCP set to true"); + // Only allow for setting one of IS_CLOUD_FORMATION, IS_GCP, or IS_AZURE to true + let cloud_providers = [is_cloud_formation, is_gcp, is_azure] + .iter() + .filter(|&&x| x) + .count(); + if cloud_providers > 1 { + panic!("Only one of IS_CLOUD_FORMATION, IS_GCP, or IS_AZURE can be set to true"); } // Error and exit if IS_GCP is true and GCP_PROJECT_ID or GCP_PROJECT_NUMBER are not set @@ -105,6 +132,16 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { panic!("GCP_PROJECT_ID and GCP_PROJECT_NUMBER must be set if IS_GCP is true"); } + // Error and exit if IS_AZURE is true and any of the required Azure environment variables are not set + if is_azure + && (azure_storage_account.is_empty() + || azure_subscription_id.is_empty() + || azure_resource_group_prefix.is_empty() + || azure_region.is_empty()) + { + panic!("AZURE_STORAGE_ACCOUNT, AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP_PREFIX, and AZURE_REGION must be set if IS_AZURE is true"); + } + // Connect to pgmq let queue = PGMQueueExt::new(pg_conn_url.clone(), 5).await?; queue.init().await?; @@ -345,6 +382,18 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { ) .await?; + init_azure_storage_workload_identity( + is_azure, + &read_msg, + &mut coredb_spec, + backup_archive_bucket.clone(), + azure_storage_account.clone(), + azure_subscription_id.clone(), + azure_resource_group_prefix.clone(), + azure_region.clone(), + ) + .await?; + info!("{}: Creating namespace", read_msg.msg_id); // create Namespace create_namespace(client.clone(), &namespace, org_id, instance_id).await?; @@ -511,6 +560,19 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { .await?; } + if is_azure { + info!( + "{}: Deleting Azure storage workload identity binding", + read_msg.msg_id + ); + delete_azure_storage_workload_identity_binding( + &azure_subscription_id, + &azure_resource_group_prefix, + &namespace, + ) + .await?; + } + let insert_query = sqlx::query!( "INSERT INTO deleted_instances (namespace) VALUES ($1) ON CONFLICT (namespace) DO NOTHING", namespace @@ -918,6 +980,7 @@ async fn init_gcp_storage_workload_identity( retentionPolicy: Some(String::from("30")), schedule: Some(generate_cron_expression(&read_msg.message.namespace)), s3_credentials: None, + azure_credentials: None, endpoint_url: None, google_credentials: Some(GoogleCredentials { gke_environment: Some(true), @@ -931,6 +994,82 @@ async fn init_gcp_storage_workload_identity( Ok(()) } +async fn init_azure_storage_workload_identity( + is_azure: bool, + read_msg: &Message, + coredb_spec: &mut CoreDBSpec, + backup_archive_bucket: String, + azure_storage_account: String, + azure_subscription_id: String, + azure_resource_group: String, + azure_region: String, +) -> Result<(), ConductorError> { + if !is_azure { + return Ok(()); + } + + let uami_client_id = create_azure_storage_workload_identity_binding( + &azure_subscription_id, + &azure_resource_group, + &azure_region, + &azure_storage_account, + &read_msg.message.namespace, + ) + .await?; + + // Format ServiceAccountTemplate spec in CoreDBSpec + use std::collections::BTreeMap; + let mut annotations: BTreeMap = BTreeMap::new(); + annotations.insert( + "azure.workload.identity/client-id".to_string(), + uami_client_id, + ); + let service_account_template = ServiceAccountTemplate { + metadata: Some(ObjectMeta { + annotations: Some(annotations), + ..ObjectMeta::default() + }), + }; + + // Generate Backup spec for CoreDB + let volume_snapshot = Some(VolumeSnapshot { + enabled: false, + snapshot_class: None, + }); + + let write_path = read_msg + .message + .backups_write_path + .clone() + .unwrap_or("v2".to_string()); + + let backup = Backup { + destinationPath: Some(format!( + "https://{}.blob.core.windows.net/{}/{}", + azure_storage_account, backup_archive_bucket, write_path + )), + encryption: Some(String::from("AES256")), + retentionPolicy: Some(String::from("30")), + schedule: Some(generate_cron_expression(&read_msg.message.namespace)), + s3_credentials: None, + azure_credentials: Some(AzureCredentials { + connection_string: None, + inherit_from_azure_ad: Some(true), + storage_account: None, + storage_key: None, + storage_sas_token: None, + }), + endpoint_url: None, + google_credentials: None, + volume_snapshot, + }; + + coredb_spec.backup = backup; + coredb_spec.serviceAccountTemplate = service_account_template; + + Ok(()) +} + fn from_env_default(key: &str, default: &str) -> String { env::var(key).unwrap_or_else(|_| default.to_owned()) }