From 26b88a01e65f9300ed9ef9de922363f027ff3dcd Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 09:37:43 -0400 Subject: [PATCH 01/27] Add AzureCredentials and dependent structs Signed-off-by: Ian Stanton --- tembo-operator/src/apis/coredb_types.rs | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tembo-operator/src/apis/coredb_types.rs b/tembo-operator/src/apis/coredb_types.rs index e327dce00..da0f4d003 100644 --- a/tembo-operator/src/apis/coredb_types.rs +++ b/tembo-operator/src/apis/coredb_types.rs @@ -190,6 +190,84 @@ pub struct GoogleCredentialsApplicationCredentials { pub name: String, } +/// AzureCredentials is the type for the credentials to be used to upload files to Azure Blob Storage. +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct AzureCredentials { + /// The connection string to be used + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "connectionString" + )] + pub connection_string: Option, + /// Use the Azure AD based authentication without providing explicitly the keys. + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "inheritFromAzureAD" + )] + pub inherit_from_azure_ad: Option, + /// The storage account where to upload data + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "storageAccount" + )] + pub storage_account: Option, + /// The storage account key to be used in conjunction with the storage account name + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "storageKey" + )] + pub storage_key: Option, + /// A shared-access-signature to be used in conjunction with the storage account name + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "storageSasToken" + )] + pub storage_sas_token: Option, +} + +/// The connection string to be used for Azure Blob Storage backups +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct AzureCredentialsConnectionString { + /// The key to select + pub key: String, + /// Name of the referent. + pub name: String, +} + +/// The storage account for Azure Blob Storage backups +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct AzureCredentialsStorageAccount { + /// The key to select + pub key: String, + /// Name of the referent. + pub name: String, +} + +/// The storage account key to be used in conjunction with the storage account name for Azure Blob +/// Storage backups +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct AzureCredentialsStorageKey { + /// The key to select + pub key: String, + /// Name of the referent. + pub name: String, +} + +/// A shared-access-signature to be used in conjunction with the storage account name for Azure Blob +/// Storage backups +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct AzureCredentialsStorageSasToken { + /// The key to select + pub key: String, + /// Name of the referent. + pub name: String, +} + /// VolumeSnapshots is the type for the configuration of the volume snapshots /// to be used for backups instead of object storage #[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema, PartialEq)] From c96bde08a7b2940a43d6ea4211de2400327adff5 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 09:49:16 -0400 Subject: [PATCH 02/27] Add to backup and restore spec Signed-off-by: Ian Stanton --- charts/tembo-operator/templates/crd.yaml | 139 ++++++++++++++++++++++- tembo-operator/src/apis/coredb_types.rs | 15 ++- 2 files changed, 147 insertions(+), 7 deletions(-) diff --git a/charts/tembo-operator/templates/crd.yaml b/charts/tembo-operator/templates/crd.yaml index 5ca875f47..11e3081f8 100644 --- a/charts/tembo-operator/templates/crd.yaml +++ b/charts/tembo-operator/templates/crd.yaml @@ -1932,6 +1932,7 @@ spec: endpointURL: null s3Credentials: null googleCredentials: null + azureCredentials: null volumeSnapshot: enabled: false description: |- @@ -1939,6 +1940,71 @@ spec: **Default**: disabled properties: + azureCredentials: + description: The Azure credentials to use for backups + nullable: true + properties: + connectionString: + description: The connection string to be used + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + inheritFromAzureAD: + description: Use the Azure AD based authentication without providing explicitly the keys. + nullable: true + type: boolean + storageAccount: + description: The storage account where to upload data + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageKey: + description: The storage account key to be used in conjunction with the storage account name + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageSasToken: + description: A shared-access-signature to be used in conjunction with the storage account name + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + type: object destinationPath: default: s3:// description: The S3 bucket path to store backups in @@ -1954,7 +2020,7 @@ spec: nullable: true type: string googleCredentials: - description: 'GoogleCredentials is the type for the credentials to be used to upload files to Google Cloud Storage. It can be provided in two alternative ways: * The secret containing the Google Cloud Storage JSON file with the credentials (applicationCredentials) * inheriting the role from the pod (GKE) environment by setting gkeEnvironment to true' + description: The Google Cloud credentials to use for backups nullable: true properties: applicationCredentials: @@ -2391,6 +2457,71 @@ spec: **Default**: disabled nullable: true properties: + azureCredentials: + description: azureCredentials is the Azure credentials to use for restores. + nullable: true + properties: + connectionString: + description: The connection string to be used + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + inheritFromAzureAD: + description: Use the Azure AD based authentication without providing explicitly the keys. + nullable: true + type: boolean + storageAccount: + description: The storage account where to upload data + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageKey: + description: The storage account key to be used in conjunction with the storage account name + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + storageSasToken: + description: A shared-access-signature to be used in conjunction with the storage account name + nullable: true + properties: + key: + description: The key to select + type: string + name: + description: Name of the referent. + type: string + required: + - key + - name + type: object + type: object backupsPath: description: |- The object storage path and bucket name of the instance you wish to restore from. This maps to the `Backup` `destinationPath` field for the original instance. @@ -2399,11 +2530,11 @@ spec: nullable: true type: string endpointURL: - description: endpointURL is the S3 compatable endpoint URL + description: endpointURL is the S3 compatible endpoint URL nullable: true type: string googleCredentials: - description: s3Credentials is the S3 credentials to use for backups. + description: googleCredentials is the Google Cloud credentials to use for restores. nullable: true properties: applicationCredentials: @@ -2428,7 +2559,7 @@ spec: nullable: true type: string s3Credentials: - description: s3Credentials is the S3 credentials to use for backups. + description: s3Credentials is the S3 credentials to use for restores. nullable: true properties: accessKeyId: diff --git a/tembo-operator/src/apis/coredb_types.rs b/tembo-operator/src/apis/coredb_types.rs index da0f4d003..3445c40c2 100644 --- a/tembo-operator/src/apis/coredb_types.rs +++ b/tembo-operator/src/apis/coredb_types.rs @@ -338,9 +338,14 @@ pub struct Backup { #[serde(rename = "s3Credentials")] pub s3_credentials: Option, + /// The Google Cloud credentials to use for backups #[serde(rename = "googleCredentials")] pub google_credentials: Option, + /// The Azure credentials to use for backups + #[serde(rename = "azureCredentials")] + pub azure_credentials: Option, + /// Enable using Volume Snapshots for backups instead of Object Storage #[serde( default = "defaults::default_volume_snapshot", @@ -393,18 +398,22 @@ pub struct Restore { #[serde(rename = "recoveryTargetTime")] pub recovery_target_time: Option, - /// endpointURL is the S3 compatable endpoint URL + /// endpointURL is the S3 compatible endpoint URL #[serde(default, rename = "endpointURL")] pub endpoint_url: Option, - /// s3Credentials is the S3 credentials to use for backups. + /// s3Credentials is the S3 credentials to use for restores. #[serde(rename = "s3Credentials")] pub s3_credentials: Option, - /// s3Credentials is the S3 credentials to use for backups. + /// googleCredentials is the Google Cloud credentials to use for restores. #[serde(rename = "googleCredentials")] pub google_credentials: Option, + /// azureCredentials is the Azure credentials to use for restores. + #[serde(rename = "azureCredentials")] + pub azure_credentials: Option, + /// volumeSnapshot is a boolean to enable restoring from a Volume Snapshot #[serde(rename = "volumeSnapshot")] pub volume_snapshot: Option, From 98eb636dfe450f11a46cfadd619aade195f8fa22 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 13:13:42 -0400 Subject: [PATCH 03/27] Add generate_azure_backup_credentials Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 67 +++++++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index d2de88d37..346f28481 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -1,5 +1,5 @@ use crate::apis::coredb_types::Restore; -use crate::apis::coredb_types::{self, GoogleCredentials}; +use crate::apis::coredb_types::{self, GoogleCredentials, AzureCredentials}; use crate::extensions::install::find_trunk_installs_to_pod; use crate::ingress_route_crd::{ IngressRoute, IngressRouteRoutes, IngressRouteRoutesKind, IngressRouteRoutesServices, @@ -80,13 +80,7 @@ use kube::{ use std::{collections::BTreeMap, sync::Arc}; use tokio::time::Duration; use tracing::{debug, error, info, instrument, warn}; - -use super::clusters::{ - ClusterBackupBarmanObjectStoreGoogleCredentials, - ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials, - ClusterExternalClustersBarmanObjectStoreGoogleCredentials, - ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials, -}; +use super::clusters::{ClusterBackupBarmanObjectStoreAzureCredentials, ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString, ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount, ClusterBackupBarmanObjectStoreAzureCredentialsStorageKey, ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken, ClusterBackupBarmanObjectStoreGoogleCredentials, ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials, ClusterExternalClustersBarmanObjectStoreGoogleCredentials, ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials}; pub struct PostgresConfig { pub postgres_parameters: Option>, @@ -157,6 +151,7 @@ fn create_cluster_backup_barman_object_store( match credentials { BackupCredentials::S3(creds) => object_store.s3_credentials = Some(creds), BackupCredentials::Google(creds) => object_store.google_credentials = Some(creds), + BackupCredentials::Azure(creds) => object_store.azure_credentials = Some(creds), } object_store @@ -213,6 +208,7 @@ fn create_cluster_backup_volume_snapshot(cdb: &CoreDB) -> ClusterBackupVolumeSna enum BackupCredentials { S3(ClusterBackupBarmanObjectStoreS3Credentials), Google(ClusterBackupBarmanObjectStoreGoogleCredentials), + Azure(ClusterBackupBarmanObjectStoreAzureCredentials), } fn create_cluster_backup( @@ -284,6 +280,8 @@ pub fn cnpg_backup_configuration( )))) } else if let Some(gcs_creds) = cdb.spec.backup.google_credentials.as_ref() { generate_google_backup_credentials(Some(gcs_creds.clone())).map(BackupCredentials::Google) + } else if let Some(azure_creds) = cdb.spec.backup.azure_credentials.as_ref() { + generate_azure_backup_credentials(Some(azure_creds.clone())).map(BackupCredentials::Azure) } else { None }; @@ -2094,6 +2092,59 @@ fn generate_s3_backup_credentials( } } +// generate_azure_backup_credentials function will generate the azure backup credentials from +// AzureCredentials object and return a ClusterBackupBarmanObjectStoreAzureCredentials object +#[instrument(fields(trace_id, creds))] +fn generate_azure_backup_credentials( + creds: Option, +) -> Option { + match creds { + Some(creds) => { + // Check if we're inheriting credentials from Azure AD + if creds.inherit_from_azure_ad.unwrap_or(false) { + Some(ClusterBackupBarmanObjectStoreAzureCredentials { + inherit_from_azure_ad: creds.inherit_from_azure_ad, + ..Default::default() + }) + } else if creds.connection_string.is_some() { + // If we're not inheriting from Azure AD, assume we are reading from a Kubernetes secret. + // https://cloudnative-pg.io/documentation/1.16/backup_recovery/#azure-blob-storage + Some(ClusterBackupBarmanObjectStoreAzureCredentials { + connection_string: creds.connection_string.as_ref().map(|cs| { + ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString { + key: cs.key.clone(), + name: cs.name.clone(), + } + }), + storage_account: creds.storage_account.as_ref().map(|sa| { + ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount { + key: sa.key.clone(), + name: sa.name.clone(), + } + }), + storage_key: creds.storage_key.as_ref().map(|sk| { + ClusterBackupBarmanObjectStoreAzureCredentialsStorageKey { + key: sk.key.clone(), + name: sk.name.clone(), + } + }), + storage_sas_token: creds.storage_sas_token.as_ref().map(|st| { + ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken { + key: st.key.clone(), + name: st.name.clone(), + } + }), + inherit_from_azure_ad: None, + }) + } + else { + None + } + } + None => None, + } +} + #[instrument(fields(trace_id, creds))] fn generate_gcs_restore_credentials( creds: Option<&GoogleCredentials>, From c4c02546d2a3fc79150a59c630773c2a227936d2 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 16:02:42 -0400 Subject: [PATCH 04/27] Add generate_azure_restore_credentials Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 64 ++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 346f28481..4ff20e58a 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -1,5 +1,21 @@ +use super::clusters::{ + ClusterBackupBarmanObjectStoreAzureCredentials, + ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString, + ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount, + ClusterBackupBarmanObjectStoreAzureCredentialsStorageKey, + ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken, + ClusterBackupBarmanObjectStoreGoogleCredentials, + ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials, + ClusterExternalClustersBarmanObjectStoreAzureCredentials, + ClusterExternalClustersBarmanObjectStoreAzureCredentialsConnectionString, + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageAccount, + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageKey, + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageSasToken, + ClusterExternalClustersBarmanObjectStoreGoogleCredentials, + ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials, +}; use crate::apis::coredb_types::Restore; -use crate::apis::coredb_types::{self, GoogleCredentials, AzureCredentials}; +use crate::apis::coredb_types::{self, AzureCredentials, GoogleCredentials}; use crate::extensions::install::find_trunk_installs_to_pod; use crate::ingress_route_crd::{ IngressRoute, IngressRouteRoutes, IngressRouteRoutesKind, IngressRouteRoutesServices, @@ -80,7 +96,6 @@ use kube::{ use std::{collections::BTreeMap, sync::Arc}; use tokio::time::Duration; use tracing::{debug, error, info, instrument, warn}; -use super::clusters::{ClusterBackupBarmanObjectStoreAzureCredentials, ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString, ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount, ClusterBackupBarmanObjectStoreAzureCredentialsStorageKey, ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken, ClusterBackupBarmanObjectStoreGoogleCredentials, ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials, ClusterExternalClustersBarmanObjectStoreGoogleCredentials, ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials}; pub struct PostgresConfig { pub postgres_parameters: Option>, @@ -381,6 +396,8 @@ pub fn cnpg_cluster_bootstrap_from_cdb( let s3_credentials = generate_s3_restore_credentials(restore.s3_credentials.as_ref()); let google_credentials = generate_gcs_restore_credentials(restore.google_credentials.as_ref()); + let azure_credentials = + generate_azure_restore_credentials(restore.azure_credentials.as_ref()); // Find destination_path from Backup to generate the restore destination path let restore_destination_path = generate_restore_destination_path(restore, &cdb.spec.backup); ClusterExternalClusters { @@ -390,6 +407,7 @@ pub fn cnpg_cluster_bootstrap_from_cdb( endpoint_url: restore.endpoint_url.clone(), s3_credentials, google_credentials, + azure_credentials, wal: Some(ClusterExternalClustersBarmanObjectStoreWal { max_parallel: Some(8), encryption: Some(ClusterExternalClustersBarmanObjectStoreWalEncryption::Aes256), @@ -2107,7 +2125,7 @@ fn generate_azure_backup_credentials( ..Default::default() }) } else if creds.connection_string.is_some() { - // If we're not inheriting from Azure AD, assume we are reading from a Kubernetes secret. + // If we're not inheriting from Azure AD, assume we are reading from a Kubernetes secret. // https://cloudnative-pg.io/documentation/1.16/backup_recovery/#azure-blob-storage Some(ClusterBackupBarmanObjectStoreAzureCredentials { connection_string: creds.connection_string.as_ref().map(|cs| { @@ -2136,8 +2154,7 @@ fn generate_azure_backup_credentials( }), inherit_from_azure_ad: None, }) - } - else { + } else { None } } @@ -2199,6 +2216,43 @@ fn generate_s3_restore_credentials( ) } +// generate_azure_restore_credentials function will generate the azure restore credentials from +// AzureCredentials object and return a ClusterExternalClustersBarmanObjectStoreAzureCredentials object +#[instrument(fields(trace_id, creds))] +fn generate_azure_restore_credentials( + creds: Option<&AzureCredentials>, +) -> Option { + creds.map( + |creds| ClusterExternalClustersBarmanObjectStoreAzureCredentials { + connection_string: creds.connection_string.as_ref().map(|cs| { + ClusterExternalClustersBarmanObjectStoreAzureCredentialsConnectionString { + key: cs.key.clone(), + name: cs.name.clone(), + } + }), + inherit_from_azure_ad: creds.inherit_from_azure_ad, + storage_account: creds.storage_account.as_ref().map(|sa| { + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageAccount { + key: sa.key.clone(), + name: sa.name.clone(), + } + }), + storage_key: creds.storage_key.as_ref().map(|sk| { + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageKey { + key: sk.key.clone(), + name: sk.name.clone(), + } + }), + storage_sas_token: creds.storage_sas_token.as_ref().map(|st| { + ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageSasToken { + key: st.key.clone(), + name: st.name.clone(), + } + }), + }, + ) +} + // is_restore_backup_running_pending_completed checks if a backup is running or // pending or completed and returns a bool or action in a result #[instrument(skip(cdb, ctx), fields(trace_id, instance_name = %cdb.name_any()))] From e804b0f65b2f4e89d635d7c6cfaa5884417bb913 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 16:14:19 -0400 Subject: [PATCH 05/27] Fix conductor test Signed-off-by: Ian Stanton --- conductor/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conductor/src/main.rs b/conductor/src/main.rs index 8d0c453c6..d0edfaf06 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -139,7 +139,7 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { loop { // Read from queue (check for new message) - // messages that dont fit a CRUDevent will error + // messages that don't fit a CRUDevent will error // set visibility timeout to 90 seconds let read_msg = queue .read::(&control_plane_events_queue, 90_i32) @@ -898,6 +898,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), From f3e81fd5c52889be980f9f76351470bbd537037d Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 16:43:43 -0400 Subject: [PATCH 06/27] Bump tembo-operator chart Signed-off-by: Ian Stanton --- charts/tembo-operator/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/tembo-operator/Chart.yaml b/charts/tembo-operator/Chart.yaml index 3da68a2e6..c635b1f73 100644 --- a/charts/tembo-operator/Chart.yaml +++ b/charts/tembo-operator/Chart.yaml @@ -3,7 +3,7 @@ name: tembo-operator description: "Helm chart to deploy the tembo-operator" type: application icon: https://cloud.tembo.io/images/TemboElephant.png -version: 0.7.3 +version: 0.8.0 home: https://tembo.io sources: - https://github.com/tembo-io/tembo From 14f9b82ff014ee38c385163975ac8a6d07b63a9d Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 23 Sep 2024 16:55:05 -0400 Subject: [PATCH 07/27] Pin google-cloud-storage crate to 0.22.0 Signed-off-by: Ian Stanton --- conductor/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conductor/Cargo.toml b/conductor/Cargo.toml index 7afbd057d..eef5de0b9 100644 --- a/conductor/Cargo.toml +++ b/conductor/Cargo.toml @@ -35,7 +35,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres"] } anyhow = "1.0.82" serde_yaml = "0.9.34" reqwest = { version = "0.12.3", features = ["json"] } -google-cloud-storage = "0.22" +google-cloud-storage = "0.22.0" [dependencies.kube] features = ["runtime", "client", "derive"] From 6620346c45b6f4f3cc2f0f0fed511fb182d4bee7 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 07:56:22 -0400 Subject: [PATCH 08/27] Add init_azure_storage Signed-off-by: Ian Stanton --- conductor/Cargo.lock | 4 +- conductor/Cargo.toml | 2 +- conductor/src/gcp/bucket_manager.rs | 2 +- conductor/src/gcp/iam_builder.rs | 4 +- conductor/src/main.rs | 83 ++++++++++++++++++++++++++--- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/conductor/Cargo.lock b/conductor/Cargo.lock index 683acbc26..b4a3e48ad 100644 --- a/conductor/Cargo.lock +++ b/conductor/Cargo.lock @@ -1455,9 +1455,9 @@ dependencies = [ [[package]] name = "google-cloud-storage" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8be793a50f2edd9d910dfb72f881f08e8b21ed34827046fc43ba6e6f1c01a7" +checksum = "c7347a3d65cd64db51e5b4aebf0c68c484042948c6d53f856f58269bc9816360" dependencies = [ "anyhow", "async-stream", diff --git a/conductor/Cargo.toml b/conductor/Cargo.toml index eef5de0b9..eeef1e9d8 100644 --- a/conductor/Cargo.toml +++ b/conductor/Cargo.toml @@ -35,7 +35,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres"] } anyhow = "1.0.82" serde_yaml = "0.9.34" reqwest = { version = "0.12.3", features = ["json"] } -google-cloud-storage = "0.22.0" +google-cloud-storage = "0.22.1" [dependencies.kube] features = ["runtime", "client", "derive"] diff --git a/conductor/src/gcp/bucket_manager.rs b/conductor/src/gcp/bucket_manager.rs index 1282a102a..1cb8813a3 100644 --- a/conductor/src/gcp/bucket_manager.rs +++ b/conductor/src/gcp/bucket_manager.rs @@ -270,7 +270,7 @@ impl BucketIamManager { fn create_bucket_condition(&self, bucket_name: &str, instance_name: &str) -> Condition { Condition { title: "allow-bucket-and-path".to_string(), - description: "Conductor managed storage bucket IAM policy condition".to_string(), + description: Some("Conductor managed storage bucket IAM policy condition".to_string()), expression: format!( r#"(resource.type == "storage.googleapis.com/Bucket") || (resource.type == "storage.googleapis.com/Object" && resource.name.startsWith("projects/_/buckets/{}/objects/{}/{}"))"#, bucket_name, BUCKET_PATH_PREFIX, instance_name diff --git a/conductor/src/gcp/iam_builder.rs b/conductor/src/gcp/iam_builder.rs index ae6cbd35c..2be771ec0 100644 --- a/conductor/src/gcp/iam_builder.rs +++ b/conductor/src/gcp/iam_builder.rs @@ -121,7 +121,7 @@ mod tests { fn test_add_condition() { let condition = Condition { title: "test".to_string(), - description: "test condition".to_string(), + description: Some("test condition".to_string()), expression: "resource.type == \"storage.googleapis.com/Bucket\") || (resource.type == \"storage.googleapis.com/Object\"".to_string(), }; let binding = IamBindingBuilder::new() @@ -138,7 +138,7 @@ mod tests { fn test_build_with_all_options() { let condition = Condition { title: "test".to_string(), - description: "test condition".to_string(), + description: Some("test condition".to_string()), expression: "resource.type == \"storage.googleapis.com/Bucket\") || (resource.type == \"storage.googleapis.com/Object\"".to_string(), }; let binding = IamBindingBuilder::new() diff --git a/conductor/src/main.rs b/conductor/src/main.rs index d0edfaf06..15e399342 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -12,9 +12,7 @@ use conductor::{ 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, -}; +use controller::apis::coredb_types::{AzureCredentials, AzureCredentialsStorageAccount, AzureCredentialsStorageKey, Backup, CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot}; use controller::apis::postgres_parameters::{ConfigValue, PgConfig}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use kube::Client; @@ -33,12 +31,12 @@ use types::{CRUDevent, Event}; mod metrics_reporter; mod status_reporter; -// Amount of time to wait after requeueing a message for an expected failure, +// Amount of time to wait after re-queueing a message for an expected failure, // where we will want to check often until it's ready. const REQUEUE_VT_SEC_SHORT: i32 = 5; -// Amount of time to wait after requeueing a message for an unexpected failure -// that we would want to try again after awhile. +// Amount of time to wait after re-queueing a message for an unexpected failure +// that we would want to try again after a while. const REQUEUE_VT_SEC_LONG: i32 = 300; async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { @@ -84,6 +82,14 @@ 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"); // Error and exit if CF_TEMPLATE_BUCKET is not set when IS_CLOUD_FORMATION is enabled if is_cloud_formation && cf_template_bucket.is_empty() { @@ -333,6 +339,15 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { ) .await?; + init_azure_storage( + is_azure, + &read_msg, + &mut coredb_spec, + backup_archive_bucket.clone(), + azure_storage_account.clone(), + ) + .await?; + info!("{}: Creating namespace", read_msg.msg_id); // create Namespace create_namespace(client.clone(), &namespace, org_id, instance_id).await?; @@ -912,6 +927,62 @@ async fn init_gcp_storage_workload_identity( Ok(()) } +async fn init_azure_storage( + is_azure: bool, + read_msg: &Message, + coredb_spec: &mut CoreDBSpec, + backup_archive_bucket: String, + azure_storage_account: String, +) -> Result<(), ConductorError> { + if !is_azure { + return Ok(()); + } + + // 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(format!("v2/{}", read_msg.message.namespace)); + + 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: None, + storage_account: Some(AzureCredentialsStorageAccount { + key: "AZURE_STORAGE_ACCOUNT".to_string(), + name: "azure-creds".to_string(), + }), + storage_key: Some(AzureCredentialsStorageKey { + key: "AZURE_STORAGE_KEY".to_string(), + name: "azure-creds".to_string(), + }), + storage_sas_token: None, + }), + endpoint_url: None, + google_credentials: None, + volume_snapshot, + }; + + coredb_spec.backup = backup; + + Ok(()) +} + fn from_env_default(key: &str, default: &str) -> String { env::var(key).unwrap_or_else(|_| default.to_owned()) } From 3dd2b9fb2b2e1a03a70896a80a1e6d3fea784553 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 08:26:19 -0400 Subject: [PATCH 09/27] fmt Signed-off-by: Ian Stanton --- conductor/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conductor/src/main.rs b/conductor/src/main.rs index 15e399342..52057f3c2 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -12,7 +12,10 @@ use conductor::{ 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::{AzureCredentials, AzureCredentialsStorageAccount, AzureCredentialsStorageKey, Backup, CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot}; +use controller::apis::coredb_types::{ + AzureCredentials, AzureCredentialsStorageAccount, AzureCredentialsStorageKey, Backup, + CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot, +}; use controller::apis::postgres_parameters::{ConfigValue, PgConfig}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; use kube::Client; From 4efe354a4b4516f05cb1e99d75acbc60725870ec Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 09:19:11 -0400 Subject: [PATCH 10/27] Add info Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 4ff20e58a..05cbffad3 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -290,12 +290,15 @@ pub fn cnpg_backup_configuration( // Copy the endpoint_url and s3_credentials from cdb to configure backups let backup_credentials = if let Some(s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { + info!("Using S3 credentials for backups"); Some(BackupCredentials::S3(generate_s3_backup_credentials(Some( s3_creds, )))) } else if let Some(gcs_creds) = cdb.spec.backup.google_credentials.as_ref() { + info!("Using Google Cloud Storage credentials for backups"); generate_google_backup_credentials(Some(gcs_creds.clone())).map(BackupCredentials::Google) } else if let Some(azure_creds) = cdb.spec.backup.azure_credentials.as_ref() { + info!("Using Azure Blob Storage credentials for backups"); generate_azure_backup_credentials(Some(azure_creds.clone())).map(BackupCredentials::Azure) } else { None From 485a3f187a7dc3865c5d0d4592eb5cec4668fbab Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 09:47:40 -0400 Subject: [PATCH 11/27] Fix logic in generate_azure_creds Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 05cbffad3..3a698789c 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -147,6 +147,7 @@ fn create_cluster_backup_barman_object_store( backup_path: &str, credentials: Option, ) -> ClusterBackupBarmanObjectStore { + info!("Using backup credentials {:?}", credentials); // For backwards compatibility, default to inherited IAM role let credentials = credentials.unwrap_or(BackupCredentials::S3( ClusterBackupBarmanObjectStoreS3Credentials { @@ -220,6 +221,7 @@ fn create_cluster_backup_volume_snapshot(cdb: &CoreDB) -> ClusterBackupVolumeSna } } +#[derive(Debug)] enum BackupCredentials { S3(ClusterBackupBarmanObjectStoreS3Credentials), Google(ClusterBackupBarmanObjectStoreGoogleCredentials), @@ -2127,16 +2129,11 @@ fn generate_azure_backup_credentials( inherit_from_azure_ad: creds.inherit_from_azure_ad, ..Default::default() }) - } else if creds.connection_string.is_some() { + } else if creds.storage_key.is_some() { // If we're not inheriting from Azure AD, assume we are reading from a Kubernetes secret. // https://cloudnative-pg.io/documentation/1.16/backup_recovery/#azure-blob-storage Some(ClusterBackupBarmanObjectStoreAzureCredentials { - connection_string: creds.connection_string.as_ref().map(|cs| { - ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString { - key: cs.key.clone(), - name: cs.name.clone(), - } - }), + connection_string: None, storage_account: creds.storage_account.as_ref().map(|sa| { ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount { key: sa.key.clone(), @@ -2149,12 +2146,7 @@ fn generate_azure_backup_credentials( name: sk.name.clone(), } }), - storage_sas_token: creds.storage_sas_token.as_ref().map(|st| { - ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken { - key: st.key.clone(), - name: st.name.clone(), - } - }), + storage_sas_token: None, inherit_from_azure_ad: None, }) } else { From c0a69344310a92b3f1bead749f3f5d6dd8d61a89 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 13:02:21 -0400 Subject: [PATCH 12/27] Check if aws for restore Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 5 ++++- conductor/src/main.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index 65321f4cc..d183e9c77 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -35,12 +35,15 @@ pub async fn generate_spec( namespace: &str, backups_bucket: &str, spec: &CoreDBSpec, + is_cloud_formation: bool, ) -> Value { let mut spec = spec.clone(); // Add the bucket name into the backups_path if it's not already there + if let Some(restore) = &mut spec.restore { if let Some(backups_path) = &mut restore.backups_path { - if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) { + if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) && is_cloud_formation + { let path_suffix = backups_path.trim_start_matches("s3://"); *backups_path = format!("s3://{}/{}", backups_bucket, path_suffix); } diff --git a/conductor/src/main.rs b/conductor/src/main.rs index 52057f3c2..dfcfdf4e9 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -375,6 +375,7 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { &namespace, &backup_archive_bucket, &coredb_spec, + is_cloud_formation, ) .await; From 62125f07bed1b678149f591d6d2a093dddfde14b Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 13:59:07 -0400 Subject: [PATCH 13/27] Hack in logic for azure restore Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 22 +++++++++++++++++++++- conductor/src/main.rs | 7 +++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index d183e9c77..c07ab7b1c 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -9,7 +9,7 @@ pub mod types; use crate::aws::cloudformation::{AWSConfigState, CloudFormationParams}; use aws_sdk_cloudformation::config::Region; -use controller::apis::coredb_types::{CoreDB, CoreDBSpec}; +use controller::apis::coredb_types::{CoreDB, CoreDBSpec, Restore}; use errors::ConductorError; use k8s_openapi::api::core::v1::{Namespace, Secret}; @@ -36,6 +36,9 @@ pub async fn generate_spec( backups_bucket: &str, spec: &CoreDBSpec, is_cloud_formation: bool, + is_azure: bool, + azure_storage_account: &str, + write_path: &str, ) -> Value { let mut spec = spec.clone(); // Add the bucket name into the backups_path if it's not already there @@ -48,7 +51,24 @@ pub async fn generate_spec( *backups_path = format!("s3://{}/{}", backups_bucket, path_suffix); } } + if is_azure { + let r = Restore { + azure_credentials: spec.backup.azure_credentials.clone(), + s3_credentials: None, + google_credentials: None, + backups_path: Some(format!( + "https://{}.blob.core.windows.net/{}/{}", + azure_storage_account, backups_bucket, write_path + )), + server_name: restore.server_name.clone(), + volume_snapshot: Some(false), + endpoint_url: None, + recovery_target_time: restore.recovery_target_time.clone(), + }; + spec.restore = Some(r); + } } + serde_json::json!({ "apiVersion": "coredb.io/v1alpha1", "kind": "CoreDB", diff --git a/conductor/src/main.rs b/conductor/src/main.rs index dfcfdf4e9..087caf4b0 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -376,6 +376,13 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { &backup_archive_bucket, &coredb_spec, is_cloud_formation, + is_azure, + &azure_storage_account, + &read_msg + .message + .backups_write_path + .clone() + .unwrap_or(format!("v2/{}", read_msg.message.namespace)), ) .await; From 04de7eb2ae48f0c01a914a766d895c7dcb2085ef Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 14:02:57 -0400 Subject: [PATCH 14/27] Fix tests Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index c07ab7b1c..56b7852ba 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -667,6 +667,10 @@ mod tests { "namespace", "my-bucket", &spec, + true, + false, + "", + "", ) .await; let expected_backups_path = "s3://my-bucket/coredb/coredb/org-coredb-inst-pgtrunkio-dev"; @@ -695,6 +699,10 @@ mod tests { "namespace", "my-bucket", &spec, + true, + false, + "", + "", ) .await; let expected_backups_path = "s3://my-bucket/coredb/coredb/org-coredb-inst-pgtrunkio-dev"; @@ -721,6 +729,10 @@ mod tests { "namespace", "my-bucket", &spec, + true, + false, + "", + "", ) .await; assert!(result["spec"]["restore"]["backupsPath"].is_null()); @@ -740,6 +752,10 @@ mod tests { "namespace", "my-bucket", &spec, + true, + false, + "", + "", ) .await; assert!(result["spec"]["restore"].is_null()); From a6ce309cde50c4976afb8fdb965e928f8f0b1300 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 14:10:34 -0400 Subject: [PATCH 15/27] Fix backups_path Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index 56b7852ba..9b44ef7c3 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -58,7 +58,7 @@ pub async fn generate_spec( google_credentials: None, backups_path: Some(format!( "https://{}.blob.core.windows.net/{}/{}", - azure_storage_account, backups_bucket, write_path + azure_storage_account, backups_bucket, restore.server_name.clone() )), server_name: restore.server_name.clone(), volume_snapshot: Some(false), From 5f42e0a102e90523471b1e80be9c8c319a6e8eae Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 24 Sep 2024 14:38:57 -0400 Subject: [PATCH 16/27] Add duplicate name to path Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index 9b44ef7c3..44245c0b9 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -57,8 +57,11 @@ pub async fn generate_spec( s3_credentials: None, google_credentials: None, backups_path: Some(format!( - "https://{}.blob.core.windows.net/{}/{}", - azure_storage_account, backups_bucket, restore.server_name.clone() + "https://{}.blob.core.windows.net/{}/{}/{}", + azure_storage_account, + backups_bucket, + restore.server_name.clone(), + restore.server_name.clone() )), server_name: restore.server_name.clone(), volume_snapshot: Some(false), From 5de7269301d88e9dd6bee527203e8c254bb5559e Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 12:12:12 -0400 Subject: [PATCH 17/27] Try to fix duplicate dir Signed-off-by: Ian Stanton --- conductor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conductor/src/main.rs b/conductor/src/main.rs index 087caf4b0..af5df283f 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -960,7 +960,7 @@ async fn init_azure_storage( .message .backups_write_path .clone() - .unwrap_or(format!("v2/{}", read_msg.message.namespace)); + .unwrap_or("v2".to_string()); let backup = Backup { destinationPath: Some(format!( From 4227c0913432175bca677e432aa23589616d6f29 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 17:59:42 -0400 Subject: [PATCH 18/27] Revert restore hack Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 42 +----------------------------------------- conductor/src/main.rs | 8 -------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index 44245c0b9..f3fe95947 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -35,43 +35,19 @@ pub async fn generate_spec( namespace: &str, backups_bucket: &str, spec: &CoreDBSpec, - is_cloud_formation: bool, - is_azure: bool, - azure_storage_account: &str, - write_path: &str, ) -> Value { let mut spec = spec.clone(); // Add the bucket name into the backups_path if it's not already there if let Some(restore) = &mut spec.restore { if let Some(backups_path) = &mut restore.backups_path { - if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) && is_cloud_formation + if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) { let path_suffix = backups_path.trim_start_matches("s3://"); *backups_path = format!("s3://{}/{}", backups_bucket, path_suffix); } } - if is_azure { - let r = Restore { - azure_credentials: spec.backup.azure_credentials.clone(), - s3_credentials: None, - google_credentials: None, - backups_path: Some(format!( - "https://{}.blob.core.windows.net/{}/{}/{}", - azure_storage_account, - backups_bucket, - restore.server_name.clone(), - restore.server_name.clone() - )), - server_name: restore.server_name.clone(), - volume_snapshot: Some(false), - endpoint_url: None, - recovery_target_time: restore.recovery_target_time.clone(), - }; - spec.restore = Some(r); - } } - serde_json::json!({ "apiVersion": "coredb.io/v1alpha1", "kind": "CoreDB", @@ -670,10 +646,6 @@ mod tests { "namespace", "my-bucket", &spec, - true, - false, - "", - "", ) .await; let expected_backups_path = "s3://my-bucket/coredb/coredb/org-coredb-inst-pgtrunkio-dev"; @@ -702,10 +674,6 @@ mod tests { "namespace", "my-bucket", &spec, - true, - false, - "", - "", ) .await; let expected_backups_path = "s3://my-bucket/coredb/coredb/org-coredb-inst-pgtrunkio-dev"; @@ -732,10 +700,6 @@ mod tests { "namespace", "my-bucket", &spec, - true, - false, - "", - "", ) .await; assert!(result["spec"]["restore"]["backupsPath"].is_null()); @@ -755,10 +719,6 @@ mod tests { "namespace", "my-bucket", &spec, - true, - false, - "", - "", ) .await; assert!(result["spec"]["restore"].is_null()); diff --git a/conductor/src/main.rs b/conductor/src/main.rs index af5df283f..a876b8038 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -375,14 +375,6 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { &namespace, &backup_archive_bucket, &coredb_spec, - is_cloud_formation, - is_azure, - &azure_storage_account, - &read_msg - .message - .backups_write_path - .clone() - .unwrap_or(format!("v2/{}", read_msg.message.namespace)), ) .await; From 8300816c9ea9678eff1730e74a66d1f35d3c8bf9 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 18:10:52 -0400 Subject: [PATCH 19/27] fmt Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index f3fe95947..be5f6f3b2 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -41,8 +41,7 @@ pub async fn generate_spec( if let Some(restore) = &mut spec.restore { if let Some(backups_path) = &mut restore.backups_path { - if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) - { + if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) { let path_suffix = backups_path.trim_start_matches("s3://"); *backups_path = format!("s3://{}/{}", backups_bucket, path_suffix); } From c3fdf84fea4b7139badd450c977c2e4f55502a36 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 18:54:40 -0400 Subject: [PATCH 20/27] Remove info Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 3a698789c..31acada6b 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -147,7 +147,6 @@ fn create_cluster_backup_barman_object_store( backup_path: &str, credentials: Option, ) -> ClusterBackupBarmanObjectStore { - info!("Using backup credentials {:?}", credentials); // For backwards compatibility, default to inherited IAM role let credentials = credentials.unwrap_or(BackupCredentials::S3( ClusterBackupBarmanObjectStoreS3Credentials { @@ -292,15 +291,12 @@ pub fn cnpg_backup_configuration( // Copy the endpoint_url and s3_credentials from cdb to configure backups let backup_credentials = if let Some(s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { - info!("Using S3 credentials for backups"); Some(BackupCredentials::S3(generate_s3_backup_credentials(Some( s3_creds, )))) } else if let Some(gcs_creds) = cdb.spec.backup.google_credentials.as_ref() { - info!("Using Google Cloud Storage credentials for backups"); generate_google_backup_credentials(Some(gcs_creds.clone())).map(BackupCredentials::Google) } else if let Some(azure_creds) = cdb.spec.backup.azure_credentials.as_ref() { - info!("Using Azure Blob Storage credentials for backups"); generate_azure_backup_credentials(Some(azure_creds.clone())).map(BackupCredentials::Azure) } else { None From 9892653a43f8a78d22a561889c1f4c4556362f7e Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 19:51:26 -0400 Subject: [PATCH 21/27] Pull out conductor changes Signed-off-by: Ian Stanton --- conductor/src/lib.rs | 3 +- conductor/src/main.rs | 85 +++---------------------------------------- 2 files changed, 6 insertions(+), 82 deletions(-) diff --git a/conductor/src/lib.rs b/conductor/src/lib.rs index be5f6f3b2..65321f4cc 100644 --- a/conductor/src/lib.rs +++ b/conductor/src/lib.rs @@ -9,7 +9,7 @@ pub mod types; use crate::aws::cloudformation::{AWSConfigState, CloudFormationParams}; use aws_sdk_cloudformation::config::Region; -use controller::apis::coredb_types::{CoreDB, CoreDBSpec, Restore}; +use controller::apis::coredb_types::{CoreDB, CoreDBSpec}; use errors::ConductorError; use k8s_openapi::api::core::v1::{Namespace, Secret}; @@ -38,7 +38,6 @@ pub async fn generate_spec( ) -> Value { let mut spec = spec.clone(); // Add the bucket name into the backups_path if it's not already there - if let Some(restore) = &mut spec.restore { if let Some(backups_path) = &mut restore.backups_path { if !backups_path.starts_with(&format!("s3://{}", backups_bucket)) { diff --git a/conductor/src/main.rs b/conductor/src/main.rs index a876b8038..8d0c453c6 100644 --- a/conductor/src/main.rs +++ b/conductor/src/main.rs @@ -13,8 +13,7 @@ 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::{ - AzureCredentials, AzureCredentialsStorageAccount, AzureCredentialsStorageKey, Backup, - CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot, + Backup, CoreDBSpec, GoogleCredentials, S3Credentials, ServiceAccountTemplate, VolumeSnapshot, }; use controller::apis::postgres_parameters::{ConfigValue, PgConfig}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; @@ -34,12 +33,12 @@ use types::{CRUDevent, Event}; mod metrics_reporter; mod status_reporter; -// Amount of time to wait after re-queueing a message for an expected failure, +// Amount of time to wait after requeueing a message for an expected failure, // where we will want to check often until it's ready. const REQUEUE_VT_SEC_SHORT: i32 = 5; -// Amount of time to wait after re-queueing a message for an unexpected failure -// that we would want to try again after a while. +// Amount of time to wait after requeueing a message for an unexpected failure +// that we would want to try again after awhile. const REQUEUE_VT_SEC_LONG: i32 = 300; async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { @@ -85,14 +84,6 @@ 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"); // Error and exit if CF_TEMPLATE_BUCKET is not set when IS_CLOUD_FORMATION is enabled if is_cloud_formation && cf_template_bucket.is_empty() { @@ -148,7 +139,7 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { loop { // Read from queue (check for new message) - // messages that don't fit a CRUDevent will error + // messages that dont fit a CRUDevent will error // set visibility timeout to 90 seconds let read_msg = queue .read::(&control_plane_events_queue, 90_i32) @@ -342,15 +333,6 @@ async fn run(metrics: CustomMetrics) -> Result<(), ConductorError> { ) .await?; - init_azure_storage( - is_azure, - &read_msg, - &mut coredb_spec, - backup_archive_bucket.clone(), - azure_storage_account.clone(), - ) - .await?; - info!("{}: Creating namespace", read_msg.msg_id); // create Namespace create_namespace(client.clone(), &namespace, org_id, instance_id).await?; @@ -916,7 +898,6 @@ 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), @@ -930,62 +911,6 @@ async fn init_gcp_storage_workload_identity( Ok(()) } -async fn init_azure_storage( - is_azure: bool, - read_msg: &Message, - coredb_spec: &mut CoreDBSpec, - backup_archive_bucket: String, - azure_storage_account: String, -) -> Result<(), ConductorError> { - if !is_azure { - return Ok(()); - } - - // 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: None, - storage_account: Some(AzureCredentialsStorageAccount { - key: "AZURE_STORAGE_ACCOUNT".to_string(), - name: "azure-creds".to_string(), - }), - storage_key: Some(AzureCredentialsStorageKey { - key: "AZURE_STORAGE_KEY".to_string(), - name: "azure-creds".to_string(), - }), - storage_sas_token: None, - }), - endpoint_url: None, - google_credentials: None, - volume_snapshot, - }; - - coredb_spec.backup = backup; - - Ok(()) -} - fn from_env_default(key: &str, default: &str) -> String { env::var(key).unwrap_or_else(|_| default.to_owned()) } From 9265eb065c25b6e772d8da40f804c7e384a142cf Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 20:02:37 -0400 Subject: [PATCH 22/27] Fill in backup values Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 31acada6b..2fae4facf 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -2129,7 +2129,12 @@ fn generate_azure_backup_credentials( // If we're not inheriting from Azure AD, assume we are reading from a Kubernetes secret. // https://cloudnative-pg.io/documentation/1.16/backup_recovery/#azure-blob-storage Some(ClusterBackupBarmanObjectStoreAzureCredentials { - connection_string: None, + connection_string: creds.connection_string.as_ref().map(|cs| { + ClusterBackupBarmanObjectStoreAzureCredentialsConnectionString { + key: cs.key.clone(), + name: cs.name.clone(), + } + }), storage_account: creds.storage_account.as_ref().map(|sa| { ClusterBackupBarmanObjectStoreAzureCredentialsStorageAccount { key: sa.key.clone(), @@ -2142,7 +2147,12 @@ fn generate_azure_backup_credentials( name: sk.name.clone(), } }), - storage_sas_token: None, + storage_sas_token: creds.storage_sas_token.as_ref().map(|st| { + ClusterBackupBarmanObjectStoreAzureCredentialsStorageSasToken { + key: st.key.clone(), + name: st.name.clone(), + } + }), inherit_from_azure_ad: None, }) } else { From 5fc5258336fd1308d45dcc8279a1f9fb3b5bdfd7 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 20:04:48 -0400 Subject: [PATCH 23/27] Remove debug Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 2fae4facf..d91060959 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -220,7 +220,6 @@ fn create_cluster_backup_volume_snapshot(cdb: &CoreDB) -> ClusterBackupVolumeSna } } -#[derive(Debug)] enum BackupCredentials { S3(ClusterBackupBarmanObjectStoreS3Credentials), Google(ClusterBackupBarmanObjectStoreGoogleCredentials), From b965fd96fb2618986abdc5081d0c4a980bcc8b1e Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Wed, 25 Sep 2024 20:53:51 -0400 Subject: [PATCH 24/27] Add tests Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 206 ++++++++++++++++++++++- 1 file changed, 204 insertions(+), 2 deletions(-) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index d91060959..788cd8016 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -3389,12 +3389,12 @@ mod tests { assert!(cdb.spec.backup.s3_credentials.is_none()); let backup_credentials = if let Some(_s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { - panic!("shouldnt get here"); + panic!("shouldn't get here"); } else if let Some(gcs_creds) = cdb.spec.backup.google_credentials.as_ref() { generate_google_backup_credentials(Some(gcs_creds.clone())) .map(BackupCredentials::Google) } else { - panic!("shouldnt get here where it's None"); + panic!("shouldn't get here where it's None"); }; let backups_result = cnpg_scheduled_backup(&cdb).unwrap(); @@ -3527,4 +3527,206 @@ mod tests { assert!(backup.is_none()); assert!(template.is_some()); } + + // Test Azure Backup configuration + fn create_azure_test_coredb() -> CoreDB { + let cdb_yaml = r#" + apiVersion: coredb.io/v1alpha1 + kind: CoreDB + metadata: + name: test + namespace: default + spec: + backup: + destinationPath: https://tembobackups.blob.core.windows.net/tembo-backups/v2/sample-standard-backup + azureCredentials: + inheritFromAzureAD: true + encryption: "AES256" + retentionPolicy: "30" + schedule: 17 9 * * * + volumeSnapshot: + enabled: true + snapshotClass: "csi-vsc" + image: quay.io/tembo/tembo-pg-cnpg:15.3.0-5-48d489e + port: 5432 + replicas: 1 + resources: + limits: + cpu: "1" + memory: 0.5Gi + serviceAccountTemplate: + metadata: + annotations: + azure.workload.identity/client-id: "16aa0b7c-dcc6-4bf5-afb5-930aa327e1aa" + labels: + azure.workload.identity/use: "true" + sharedirStorage: 1Gi + stop: false + storage: 1Gi + storageClass: "gp3-enc" + uid: 999 + "#; + + serde_yaml::from_str(cdb_yaml).expect("Failed to parse YAML") + } + + #[test] + fn test_create_cluster_backup_default_azure() { + let cdb = create_azure_test_coredb(); + let snapshot = create_cluster_backup_volume_snapshot(&cdb); + let endpoint_url = cdb.spec.backup.endpoint_url.clone(); + let backup_path = cdb.spec.backup.destinationPath.clone(); + + assert!(cdb.spec.backup.s3_credentials.is_none()); + assert!(cdb.spec.backup.google_credentials.is_none()); + + let backup_credentials = if let Some(_s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { + panic!("shouldn't get here"); + } else if let Some(gcs_creds) = cdb.spec.backup.google_credentials.as_ref() { + panic!("shouldn't get here"); + } else if let Some(azure_creds) = cdb.spec.backup.azure_credentials.as_ref() { + generate_azure_backup_credentials(Some(azure_creds.clone())) + .map(BackupCredentials::Azure) + } else { + panic!("shouldn't get here where it's None"); + }; + + let backups_result = cnpg_scheduled_backup(&cdb).unwrap(); + let (scheduled_backup, volume_snapshot_backup) = &backups_result[0]; + + let result = create_cluster_backup( + &cdb, + endpoint_url, + &backup_path.unwrap(), + backup_credentials, + ); + assert!(result.is_some()); + let backup = result.unwrap(); + + match backup.barman_object_store { + Some(barman_store) => { + // Assert to make sure that the destination path is set correctly and starts with `https://` + assert!( + barman_store.destination_path.starts_with("https://"), + "Destination path should start with 'https://', but got: {}", + barman_store.destination_path + ); + + // Check Azure credentials + match barman_store.azure_credentials { + Some(az_credentials) => { + assert_eq!( + az_credentials.inherit_from_azure_ad, + Some(true), + "Expected inheritFromAzureAD to be true, but got: {:?}", + az_credentials.inherit_from_azure_ad + ); + } + None => panic!("Expected Azure credentials to be Some, but got None"), + } + } + None => panic!("Expected barman_object_store to be Some, but got None"), + } + + // Set an expected ClusterBackupVolumeSnapshot object + let expected_snapshot = ClusterBackupVolumeSnapshot { + class_name: Some("csi-vsc".to_string()), // Expected to match the YAML input + online: Some(true), + online_configuration: Some(ClusterBackupVolumeSnapshotOnlineConfiguration { + wait_for_archive: Some(true), + immediate_checkpoint: Some(true), + }), + snapshot_owner_reference: Some( + ClusterBackupVolumeSnapshotSnapshotOwnerReference::Cluster, + ), + ..ClusterBackupVolumeSnapshot::default() + }; + + // Assert to make sure that the snapshot.snapshot_class and expected_snapshot.snapshot_class are the same + assert_eq!(snapshot, expected_snapshot); + + // Assert to make sure that the ScheduledBackup method is set to VolumeSnapshot + if let Some(volume_snapshot_backup) = volume_snapshot_backup { + assert_eq!( + volume_snapshot_backup.spec.method, + Some(ScheduledBackupMethod::VolumeSnapshot) + ); + } else { + panic!("Expected volume snapshot backup to be Some, but was None"); + } + + // Assert to make sure that the ScheduledBackup method is set to BarmanObjectStore + assert_eq!( + scheduled_backup.spec.method, + Some(ScheduledBackupMethod::BarmanObjectStore) + ); + } + + #[test] + fn test_azure_backup_configuration() { + let cdb = create_azure_test_coredb(); + let cfg = Config { + enable_backup: true, + enable_volume_snapshot: true, + reconcile_ttl: 30, + reconcile_timestamp_ttl: 90, + }; + + // Test with backups enabled and valid path + let (backup, template) = cnpg_backup_configuration(&cdb, &cfg); + assert!(backup.is_some()); + assert!(template.is_some()); + + // Verify backup configuration + if let Some(backup) = backup { + assert_eq!( + backup + .barman_object_store + .as_ref() + .map(|bos| bos.destination_path.as_str()), + Some("https://tembobackups.blob.core.windows.net/tembo-backups/v2/sample-standard-backup") + ); + assert_eq!(backup.retention_policy.as_deref(), Some("30d")); + assert!(backup.volume_snapshot.is_some()); + assert_eq!( + backup.volume_snapshot.as_ref().and_then(|vs| vs.online), + Some(true) + ); + assert_eq!( + backup.volume_snapshot.and_then(|vs| vs.class_name), + Some("csi-vsc".to_string()) + ); + } + + // Verify service account template + if let Some(template) = template { + assert_eq!( + template + .metadata + .annotations + .as_ref() + .and_then(|annots| annots.get("azure.workload.identity/client-id")), + Some(&"16aa0b7c-dcc6-4bf5-afb5-930aa327e1aa".to_string()) + ); + assert_eq!( + template + .metadata + .labels + .as_ref() + .and_then(|labels| labels.get("azure.workload.identity/use")), + Some(&"true".to_string()) + ); + } + + // Test with backups disabled + let cfg_disabled = Config { + enable_backup: false, + enable_volume_snapshot: false, + reconcile_ttl: 30, + reconcile_timestamp_ttl: 90, + }; + let (backup, template) = cnpg_backup_configuration(&cdb, &cfg_disabled); + assert!(backup.is_none()); + assert!(template.is_some()); + } } From f665857a586944df9eabc0827f109c7e74691643 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Thu, 26 Sep 2024 17:30:06 -0400 Subject: [PATCH 25/27] Add inheritedMetadata if azure and workload identity Signed-off-by: Ian Stanton --- tembo-operator/src/cloudnativepg/cnpg.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index 788cd8016..b8a4d7ef7 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -13,6 +13,7 @@ use super::clusters::{ ClusterExternalClustersBarmanObjectStoreAzureCredentialsStorageSasToken, ClusterExternalClustersBarmanObjectStoreGoogleCredentials, ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials, + ClusterInheritedMetadata, }; use crate::apis::coredb_types::Restore; use crate::apis::coredb_types::{self, AzureCredentials, GoogleCredentials}; @@ -618,6 +619,27 @@ fn default_cluster_annotations(cdb: &CoreDB) -> BTreeMap { annotations } +// Check if the cluster has azure credentials and return the inherited metadata +// required for workload identity +fn inherited_metadata(cdb: &CoreDB) -> Option { + if let Some(azure_creds) = &cdb.spec.backup.azure_credentials { + if azure_creds.inherit_from_azure_ad? { + return Some(ClusterInheritedMetadata { + annotations: None, + labels: Some( + vec![( + "azure.workload.identity/use".to_string(), + "true".to_string(), + )] + .into_iter() + .collect(), + ), + }); + } + } + None +} + #[instrument(skip(cdb), fields(trace_id, instance_name = %cdb.name_any()))] pub fn cnpg_cluster_from_cdb( cdb: &CoreDB, @@ -711,6 +733,7 @@ pub fn cnpg_cluster_from_cdb( enable_superuser_access: Some(true), failover_delay: Some(0), image_name: Some(image), + inherited_metadata: inherited_metadata(cdb), instances, log_level: Some(ClusterLogLevel::Info), managed: cluster_managed(&name), From 34a34422a2774efc32ba461d6496dee8971d59ff Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Mon, 21 Oct 2024 16:42:48 -0400 Subject: [PATCH 26/27] `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()) } From fb2f431f6ba0d7c16cf28b182f42b0007abc7bf3 Mon Sep 17 00:00:00 2001 From: Ian Stanton Date: Tue, 22 Oct 2024 09:58:39 -0400 Subject: [PATCH 27/27] Bump controller crate Signed-off-by: Ian Stanton --- conductor/Cargo.lock | 2 +- tembo-operator/Cargo.lock | 2 +- tembo-operator/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conductor/Cargo.lock b/conductor/Cargo.lock index b8713f0a1..939f10066 100644 --- a/conductor/Cargo.lock +++ b/conductor/Cargo.lock @@ -1100,7 +1100,7 @@ checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" [[package]] name = "controller" -version = "0.50.2" +version = "0.51.0" dependencies = [ "actix-web", "anyhow", diff --git a/tembo-operator/Cargo.lock b/tembo-operator/Cargo.lock index 576ca6dd5..0885fc51d 100644 --- a/tembo-operator/Cargo.lock +++ b/tembo-operator/Cargo.lock @@ -503,7 +503,7 @@ dependencies = [ [[package]] name = "controller" -version = "0.50.2" +version = "0.51.0" dependencies = [ "actix-web", "anyhow", diff --git a/tembo-operator/Cargo.toml b/tembo-operator/Cargo.toml index 55c2a291c..844de4bd2 100644 --- a/tembo-operator/Cargo.toml +++ b/tembo-operator/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "controller" description = "Tembo Operator for Postgres" -version = "0.50.2" +version = "0.51.0" edition = "2021" default-run = "controller" license = "Apache-2.0"