diff --git a/.github/workflows/conductor.yaml b/.github/workflows/conductor.yaml index 9eb9d6da7..e246fdc8c 100644 --- a/.github/workflows/conductor.yaml +++ b/.github/workflows/conductor.yaml @@ -32,7 +32,7 @@ jobs: fail-fast: false matrix: kube_version: - - "1.25.8" + - "1.29.8" steps: - uses: actions/checkout@v4 - name: Install system dependencies diff --git a/.github/workflows/operator.yaml b/.github/workflows/operator.yaml index 7e0e19e3d..ae0254d87 100644 --- a/.github/workflows/operator.yaml +++ b/.github/workflows/operator.yaml @@ -16,14 +16,14 @@ on: branches: - main paths: - - '.github/workflows/operator.yaml' - - 'tembo-operator/**' + - ".github/workflows/operator.yaml" + - "tembo-operator/**" push: branches: - main paths: - - '.github/workflows/operator.yaml' - - 'tembo-operator/**' + - ".github/workflows/operator.yaml" + - "tembo-operator/**" jobs: functional_test: @@ -39,7 +39,7 @@ jobs: # Go here for a list of versions: # https://github.com/kubernetes-sigs/kind/releases kube_version: - - '1.25.8' + - "1.29.8" steps: - uses: actions/checkout@v4 - name: Install system dependencies diff --git a/charts/tembo-operator/Chart.lock b/charts/tembo-operator/Chart.lock index 6b1be2d6b..ba2804e61 100644 --- a/charts/tembo-operator/Chart.lock +++ b/charts/tembo-operator/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: cloudnative-pg repository: https://cloudnative-pg.github.io/charts - version: 0.20.1 -digest: sha256:8b7ed89dc3d149784f369ed4035d79268e9348f232b5cbebd5096c2d29e9ded7 -generated: "2024-02-12T14:57:18.051558882-06:00" + version: 0.21.6 +digest: sha256:3922d990e9dec07c6dda1f7b8799e9cfd2ef28450357f5a3f260a3d4773e5db2 +generated: "2024-09-04T09:47:10.610286988-05:00" diff --git a/charts/tembo-operator/Chart.yaml b/charts/tembo-operator/Chart.yaml index b2580a572..3da68a2e6 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.2 +version: 0.7.3 home: https://tembo.io sources: - https://github.com/tembo-io/tembo @@ -17,6 +17,6 @@ maintainers: url: https://tembocommunity.slack.com dependencies: - name: cloudnative-pg - version: 0.20.1 + version: 0.21.6 repository: https://cloudnative-pg.github.io/charts condition: cloudnative-pg.enabled diff --git a/charts/tembo-operator/charts/cloudnative-pg-0.20.1.tgz b/charts/tembo-operator/charts/cloudnative-pg-0.20.1.tgz deleted file mode 100644 index fc7d3a83c..000000000 Binary files a/charts/tembo-operator/charts/cloudnative-pg-0.20.1.tgz and /dev/null differ diff --git a/charts/tembo-operator/charts/cloudnative-pg-0.21.6.tgz b/charts/tembo-operator/charts/cloudnative-pg-0.21.6.tgz new file mode 100644 index 000000000..3028f0f66 Binary files /dev/null and b/charts/tembo-operator/charts/cloudnative-pg-0.21.6.tgz differ diff --git a/charts/tembo-operator/templates/crd.yaml b/charts/tembo-operator/templates/crd.yaml index ea445eed8..5ca875f47 100644 --- a/charts/tembo-operator/templates/crd.yaml +++ b/charts/tembo-operator/templates/crd.yaml @@ -1930,8 +1930,8 @@ spec: retentionPolicy: '30' schedule: 0 0 * * * endpointURL: null - s3Credentials: - inheritFromIAMRole: true + s3Credentials: null + googleCredentials: null volumeSnapshot: enabled: false description: |- @@ -1953,14 +1953,33 @@ spec: description: The S3 compatable endpoint URL 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' + nullable: true + properties: + applicationCredentials: + description: The reference to the secret containing the Google Cloud Storage JSON file with the credentials + nullable: true + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + gkeEnvironment: + description: Use the role based authentication without providing explicitly the keys. + nullable: true + type: boolean + type: object retentionPolicy: default: '30' description: The number of days to retain backups for nullable: true type: string s3Credentials: - default: - inheritFromIAMRole: true description: The S3 credentials to use for backups (if not using IAM Role) nullable: true properties: @@ -2383,6 +2402,27 @@ spec: description: endpointURL is the S3 compatable endpoint URL nullable: true type: string + googleCredentials: + description: s3Credentials is the S3 credentials to use for backups. + nullable: true + properties: + applicationCredentials: + description: The reference to the secret containing the Google Cloud Storage JSON file with the credentials + nullable: true + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + gkeEnvironment: + description: Use the role based authentication without providing explicitly the keys. + nullable: true + type: boolean + type: object recoveryTargetTime: description: recovery_target_time is the time base target for point-in-time recovery. nullable: true diff --git a/conductor/Cargo.lock b/conductor/Cargo.lock index cc50e55d0..ce65c0a26 100644 --- a/conductor/Cargo.lock +++ b/conductor/Cargo.lock @@ -850,7 +850,7 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "controller" -version = "0.49.10" +version = "0.50.0" dependencies = [ "actix-web", "anyhow", diff --git a/conductor/justfile b/conductor/justfile index 7d54798d5..d057e4caa 100644 --- a/conductor/justfile +++ b/conductor/justfile @@ -2,7 +2,7 @@ NAME := "conductor" VERSION := `git rev-parse HEAD` SEMVER_VERSION := `grep version Cargo.toml | awk -F"\"" '{print $2}' | head -n 1` NAMESPACE := "default" -KUBE_VERSION := env_var_or_default('KUBE_VERSION', '1.25.8') +KUBE_VERSION := env_var_or_default('KUBE_VERSION', '1.29.8') RUST_LOG := "info" default: @@ -10,9 +10,7 @@ default: install-traefik: kubectl create namespace traefik || true - helm upgrade --install --namespace=traefik --version=20.8.0 --values=./testdata/traefik-values.yaml traefik traefik/traefik - # https://github.com/traefik/traefik-helm-chart/issues/757#issuecomment-1753995542 - kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.0.0-beta2/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml + helm upgrade --install --namespace=traefik --version=29.0.1 --values=./testdata/traefik-values.yaml traefik traefik/traefik install-operator: just install-cert-manager diff --git a/conductor/testdata/traefik-values.yaml b/conductor/testdata/traefik-values.yaml index 6ef225bbf..bbaab8334 100644 --- a/conductor/testdata/traefik-values.yaml +++ b/conductor/testdata/traefik-values.yaml @@ -1,5 +1,7 @@ image: - tag: v3.0.0-beta2 + tag: v3.0.3-tembo.1 + registry: quay.io/tembo + repository: traefik logs: general: level: DEBUG @@ -15,22 +17,38 @@ additionalArguments: - "--api.debug=true" ports: postgresql: - expose: true + expose: + default: true port: 5432 exposedPort: 5432 nodePort: 32432 protocol: TCP - web: - expose: true - port: 8080 - exposedPort: 8080 - nodePort: 32430 + # web: + # expose: true + # port: 8080 + # exposedPort: 8080 + # nodePort: 32430 + # protocol: TCP + websecure: + expose: + default: true + port: 8443 + exposedPort: 8443 + nodePort: 32443 protocol: TCP traefik: - expose: true + expose: + default: true port: 9000 exposedPort: 9000 nodePort: 32431 protocol: TCP deployment: replicas: 1 +resources: + requests: + cpu: "200m" + memory: "100Mi" + limits: + cpu: "400m" + memory: "300Mi" diff --git a/tembo-operator/Cargo.lock b/tembo-operator/Cargo.lock index 786613cab..24f55c952 100644 --- a/tembo-operator/Cargo.lock +++ b/tembo-operator/Cargo.lock @@ -503,7 +503,7 @@ dependencies = [ [[package]] name = "controller" -version = "0.49.10" +version = "0.50.0" dependencies = [ "actix-web", "anyhow", diff --git a/tembo-operator/Cargo.toml b/tembo-operator/Cargo.toml index 2bdd5d61c..367a22518 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.49.10" +version = "0.50.0" edition = "2021" default-run = "controller" license = "Apache-2.0" diff --git a/tembo-operator/src/apis/coredb_types.rs b/tembo-operator/src/apis/coredb_types.rs index 55363fef6..e327dce00 100644 --- a/tembo-operator/src/apis/coredb_types.rs +++ b/tembo-operator/src/apis/coredb_types.rs @@ -160,6 +160,36 @@ pub struct S3CredentialsSessionToken { pub name: String, } +/// 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 +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct GoogleCredentials { + /// The reference to the secret containing the Google Cloud Storage JSON file with the credentials + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "applicationCredentials" + )] + pub application_credentials: Option, + + /// Use the role based authentication without providing explicitly the keys. + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "gkeEnvironment" + )] + pub gke_environment: Option, +} + +/// GoogleCredentialsApplicationCredentials is the type for the reference to the secret containing the Google Cloud Storage JSON file with the credentials +#[derive(Serialize, Deserialize, Clone, Debug, Default, JsonSchema)] +pub struct GoogleCredentialsApplicationCredentials { + pub key: String, + 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)] @@ -227,9 +257,12 @@ pub struct Backup { pub endpoint_url: Option, /// The S3 credentials to use for backups (if not using IAM Role) - #[serde(default = "defaults::default_s3_credentials", rename = "s3Credentials")] + #[serde(rename = "s3Credentials")] pub s3_credentials: Option, + #[serde(rename = "googleCredentials")] + pub google_credentials: Option, + /// Enable using Volume Snapshots for backups instead of Object Storage #[serde( default = "defaults::default_volume_snapshot", @@ -290,6 +323,10 @@ pub struct Restore { #[serde(rename = "s3Credentials")] pub s3_credentials: Option, + /// s3Credentials is the S3 credentials to use for backups. + #[serde(rename = "googleCredentials")] + pub google_credentials: Option, + /// volumeSnapshot is a boolean to enable restoring from a Volume Snapshot #[serde(rename = "volumeSnapshot")] pub volume_snapshot: Option, diff --git a/tembo-operator/src/cloudnativepg/cnpg.rs b/tembo-operator/src/cloudnativepg/cnpg.rs index e5012abc9..d2de88d37 100644 --- a/tembo-operator/src/cloudnativepg/cnpg.rs +++ b/tembo-operator/src/cloudnativepg/cnpg.rs @@ -1,5 +1,5 @@ -use crate::apis::coredb_types; use crate::apis::coredb_types::Restore; +use crate::apis::coredb_types::{self, GoogleCredentials}; use crate::extensions::install::find_trunk_installs_to_pod; use crate::ingress_route_crd::{ IngressRoute, IngressRouteRoutes, IngressRouteRoutesKind, IngressRouteRoutesServices, @@ -81,6 +81,13 @@ use std::{collections::BTreeMap, sync::Arc}; use tokio::time::Duration; use tracing::{debug, error, info, instrument, warn}; +use super::clusters::{ + ClusterBackupBarmanObjectStoreGoogleCredentials, + ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials, + ClusterExternalClustersBarmanObjectStoreGoogleCredentials, + ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials, +}; + pub struct PostgresConfig { pub postgres_parameters: Option>, pub shared_preload_libraries: Option>, @@ -127,18 +134,32 @@ fn create_cluster_backup_barman_wal(cdb: &CoreDB) -> Option, backup_path: &str, - s3_credentials: &ClusterBackupBarmanObjectStoreS3Credentials, + credentials: Option, ) -> ClusterBackupBarmanObjectStore { - ClusterBackupBarmanObjectStore { + // For backwards compatibility, default to inherited IAM role + let credentials = credentials.unwrap_or(BackupCredentials::S3( + ClusterBackupBarmanObjectStoreS3Credentials { + inherit_from_iam_role: Some(true), + ..Default::default() + }, + )); + + let mut object_store = ClusterBackupBarmanObjectStore { data: create_cluster_backup_barman_data(cdb), - endpoint_url: Some(endpoint_url.to_string()), + endpoint_url, destination_path: backup_path.to_string(), - s3_credentials: Some(s3_credentials.clone()), wal: create_cluster_backup_barman_wal(cdb), ..ClusterBackupBarmanObjectStore::default() + }; + + match credentials { + BackupCredentials::S3(creds) => object_store.s3_credentials = Some(creds), + BackupCredentials::Google(creds) => object_store.google_credentials = Some(creds), } + + object_store } fn create_cluster_certificates(cdb: &CoreDB) -> Option { @@ -189,11 +210,16 @@ fn create_cluster_backup_volume_snapshot(cdb: &CoreDB) -> ClusterBackupVolumeSna } } +enum BackupCredentials { + S3(ClusterBackupBarmanObjectStoreS3Credentials), + Google(ClusterBackupBarmanObjectStoreGoogleCredentials), +} + fn create_cluster_backup( cdb: &CoreDB, - endpoint_url: &str, + endpoint_url: Option, backup_path: &str, - s3_credentials: &ClusterBackupBarmanObjectStoreS3Credentials, + credentials: Option, ) -> Option { let retention_days = match &cdb.spec.backup.retentionPolicy { None => "30d".to_string(), @@ -221,7 +247,7 @@ fn create_cluster_backup( cdb, endpoint_url, backup_path, - s3_credentials, + credentials, )), retention_policy: Some(retention_days), volume_snapshot, @@ -233,103 +259,41 @@ pub fn cnpg_backup_configuration( cdb: &CoreDB, cfg: &Config, ) -> (Option, Option) { - let mut service_account_template: Option = None; - - // Check if backups are enabled - if !cfg.enable_backup { - return (None, None); - } - - debug!("Backups are enabled, configuring..."); + let requested_service_account = cdb.spec.serviceAccountTemplate.clone(); + let service_account_template = match requested_service_account.metadata { + Some(metadata) => Some(ClusterServiceAccountTemplate { + metadata: ClusterServiceAccountTemplateMetadata { + annotations: metadata.annotations, + labels: metadata.labels, + }, + }), + None => None, + }; - // Check for backup path let backup_path = cdb.spec.backup.destinationPath.clone(); - if backup_path.is_none() { - warn!("Backups are disabled because we don't have an S3 backup path"); - return (None, None); - } - - let should_set_service_account_template = (cdb.spec.backup.endpoint_url.is_none() - && cdb.spec.backup.s3_credentials.is_none()) - || (cdb - .spec - .backup - .s3_credentials - .as_ref() - .and_then(|cred| cred.inherit_from_iam_role) - .unwrap_or(false) - && cdb - .spec - .serviceAccountTemplate - .metadata - .as_ref() - .and_then(|meta| meta.annotations.as_ref()) - .map_or(false, |annots| { - annots.contains_key("eks.amazonaws.com/role-arn") - })); - let should_reset_service_account_template = cdb - .spec - .backup - .s3_credentials - .as_ref() - .and_then(|cred| cred.inherit_from_iam_role) - == Some(false) - && (cdb - .spec - .backup - .s3_credentials - .as_ref() - .map_or(false, |cred| { - cred.access_key_id.is_some() - || cred.region.is_some() - || cred.secret_access_key.is_some() - || cred.session_token.is_some() - })); - - if should_reset_service_account_template { - service_account_template = None; - } else if should_set_service_account_template { - let service_account_metadata = cdb.spec.serviceAccountTemplate.metadata.clone(); - if service_account_metadata.is_none() { - warn!("Backups are disabled because we don't have a service account template"); - return (None, None); - } - let service_account_annotations = service_account_metadata - .expect("Expected service account template metadata") - .annotations; - if service_account_annotations.is_none() { - warn!("Backups are disabled because we don't have a service account template with annotations"); - return (None, None); - } - let annotations = - service_account_annotations.expect("Expected service account template annotations"); - let service_account_role_arn = annotations.get("eks.amazonaws.com/role-arn"); - if service_account_role_arn.is_none() { - warn!( - "Backups are disabled because we don't have a service account template with an EKS role ARN" - ); - return (None, None); - } - let role_arn = service_account_role_arn - .expect("Expected service account template annotations to contain an EKS role ARN") - .clone(); - - service_account_template = Some(ClusterServiceAccountTemplate { - metadata: ClusterServiceAccountTemplateMetadata { - annotations: Some(BTreeMap::from([( - "eks.amazonaws.com/role-arn".to_string(), - role_arn, - )])), - ..ClusterServiceAccountTemplateMetadata::default() - }, - }); + if !cfg.enable_backup || backup_path.is_none() { + return (None, service_account_template); } + let backup_path = backup_path.unwrap(); + // Copy the endpoint_url and s3_credentials from cdb to configure backups - let endpoint_url = cdb.spec.backup.endpoint_url.as_deref().unwrap_or_default(); - let s3_credentials = generate_s3_backup_credentials(cdb.spec.backup.s3_credentials.as_ref()); - let cluster_backup = - create_cluster_backup(cdb, endpoint_url, &backup_path.unwrap(), &s3_credentials); + let backup_credentials = if let Some(s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { + Some(BackupCredentials::S3(generate_s3_backup_credentials(Some( + s3_creds, + )))) + } 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 { + None + }; + + let cluster_backup = create_cluster_backup( + cdb, + cdb.spec.backup.endpoint_url.clone(), + &backup_path, + backup_credentials, + ); (cluster_backup, service_account_template) } @@ -415,7 +379,10 @@ pub fn cnpg_cluster_bootstrap_from_cdb( let superuser_secret_name = format!("{}-connection", cluster_name); let coredb_cluster = if let Some(restore) = &cdb.spec.restore { + // This generates the default is s3_credentials was none let s3_credentials = generate_s3_restore_credentials(restore.s3_credentials.as_ref()); + let google_credentials = + generate_gcs_restore_credentials(restore.google_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 { @@ -423,7 +390,8 @@ pub fn cnpg_cluster_bootstrap_from_cdb( barman_object_store: Some(ClusterExternalClustersBarmanObjectStore { destination_path: restore_destination_path, endpoint_url: restore.endpoint_url.clone(), - s3_credentials: Some(s3_credentials), + s3_credentials, + google_credentials, wal: Some(ClusterExternalClustersBarmanObjectStoreWal { max_parallel: Some(8), encryption: Some(ClusterExternalClustersBarmanObjectStoreWalEncryption::Aes256), @@ -2046,6 +2014,37 @@ fn generate_restore_destination_path(restore: &Restore, backup: &coredb_types::B } } +// generate_google_backup_credentials function will generate the google backup credentials from +// GoogleCredentials object and return a ClusterBackupBarmanObjectStoreGoogleCredentials object +#[instrument(fields(trace_id, creds))] +fn generate_google_backup_credentials( + creds: Option, +) -> Option { + match creds { + Some(creds) => { + if creds.application_credentials.is_some() { + Some(ClusterBackupBarmanObjectStoreGoogleCredentials { + application_credentials: creds.application_credentials.as_ref().map(|app| { + ClusterBackupBarmanObjectStoreGoogleCredentialsApplicationCredentials { + key: app.key.clone(), + name: app.name.clone(), + } + }), + gke_environment: Some(false), + }) + } else if creds.gke_environment.unwrap_or(false) { + Some(ClusterBackupBarmanObjectStoreGoogleCredentials { + gke_environment: Some(true), + application_credentials: None, + }) + } else { + None + } + } + None => None, + } +} + // generate_s3_backup_credentials function will generate the s3 backup credentials from // S3Credentials object and return a ClusterBackupBarmanObjectStoreS3Credentials object #[instrument(fields(trace_id, creds))] @@ -2095,28 +2094,38 @@ fn generate_s3_backup_credentials( } } +#[instrument(fields(trace_id, creds))] +fn generate_gcs_restore_credentials( + creds: Option<&GoogleCredentials>, +) -> Option { + creds.map( + |creds| ClusterExternalClustersBarmanObjectStoreGoogleCredentials { + application_credentials: creds.application_credentials.as_ref().map(|ac| { + ClusterExternalClustersBarmanObjectStoreGoogleCredentialsApplicationCredentials { + key: ac.key.clone(), + name: ac.name.clone(), + } + }), + gke_environment: creds.gke_environment, + }, + ) +} + // generate_s3_restore_credentials function will generate the s3 restore credentials from // S3Credentials object and return a ClusterExternalClustersBarmanObjectStoreS3Credentials object #[instrument(fields(trace_id, creds))] fn generate_s3_restore_credentials( creds: Option<&S3Credentials>, -) -> ClusterExternalClustersBarmanObjectStoreS3Credentials { - if let Some(creds) = creds { - if creds.access_key_id.is_none() && creds.secret_access_key.is_none() { - return ClusterExternalClustersBarmanObjectStoreS3Credentials { - inherit_from_iam_role: Some(true), - ..Default::default() - }; - } - - ClusterExternalClustersBarmanObjectStoreS3Credentials { +) -> Option { + creds.map( + |creds| ClusterExternalClustersBarmanObjectStoreS3Credentials { access_key_id: creds.access_key_id.as_ref().map(|id| { ClusterExternalClustersBarmanObjectStoreS3CredentialsAccessKeyId { key: id.key.clone(), name: id.name.clone(), } }), - inherit_from_iam_role: Some(false), + inherit_from_iam_role: creds.inherit_from_iam_role, region: creds.region.as_ref().map(|r| { ClusterExternalClustersBarmanObjectStoreS3CredentialsRegion { key: r.key.clone(), @@ -2135,13 +2144,8 @@ fn generate_s3_restore_credentials( name: token.name.clone(), } }), - } - } else { - ClusterExternalClustersBarmanObjectStoreS3Credentials { - inherit_from_iam_role: Some(true), - ..Default::default() - } - } + }, + ) } // is_restore_backup_running_pending_completed checks if a backup is running or @@ -3229,4 +3233,193 @@ mod tests { "stormy-capybara-snap" ); } + + // Test GCP Backup configuration + fn create_gke_test_coredb() -> CoreDB { + let cdb_yaml = r#" + apiVersion: coredb.io/v1alpha1 + kind: CoreDB + metadata: + name: test + namespace: default + spec: + backup: + destinationPath: gs://tembo-backup/sample-standard-backup + googleCredentials: + gkeEnvironment: 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: + iam.gke.io/gcp-service-account: tembo-operator-test-abc123@test-123456.iam.gserviceaccount.com + 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_google() { + let cdb = create_gke_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()); + + let backup_credentials = if let Some(_s3_creds) = cdb.spec.backup.s3_credentials.as_ref() { + panic!("shouldnt 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"); + }; + + 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 `gs://` + assert!( + barman_store.destination_path.starts_with("gs://"), + "Destination path should start with 'gs://', but got: {}", + barman_store.destination_path + ); + + // Check Google credentials + match barman_store.google_credentials { + Some(goog_credentials) => { + assert_eq!( + goog_credentials.gke_environment, + Some(true), + "Expected GKE environment to be true, but got: {:?}", + goog_credentials.gke_environment + ); + } + None => panic!("Expected Google 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_cnpg_backup_configuration() { + let cdb = create_gke_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("gs://tembo-backup/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("iam.gke.io/gcp-service-account")), + Some(&"tembo-operator-test-abc123@test-123456.iam.gserviceaccount.com".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()); + } } diff --git a/tembo-operator/src/defaults.rs b/tembo-operator/src/defaults.rs index f2d235e01..db6c36f87 100644 --- a/tembo-operator/src/defaults.rs +++ b/tembo-operator/src/defaults.rs @@ -1,7 +1,7 @@ use crate::apis::coredb_types::CoreDB; use crate::{ apis::coredb_types::{ - Backup, ConnectionPooler, PgBouncer, S3Credentials, ServiceAccountTemplate, VolumeSnapshot, + Backup, ConnectionPooler, PgBouncer, ServiceAccountTemplate, VolumeSnapshot, }, cloudnativepg::clusters::ClusterAffinity, cloudnativepg::poolers::{PoolerPgbouncerPoolMode, PoolerTemplateSpecContainersResources}, @@ -162,7 +162,6 @@ pub fn default_backup() -> Backup { encryption: default_encryption(), retentionPolicy: default_retention_policy(), schedule: default_backup_schedule(), - s3_credentials: default_s3_credentials(), volume_snapshot: default_volume_snapshot(), ..Default::default() } @@ -229,13 +228,6 @@ pub fn default_pgbouncer() -> PgBouncer { } } -pub fn default_s3_credentials() -> Option { - Some(S3Credentials { - inherit_from_iam_role: Some(true), - ..Default::default() - }) -} - pub fn default_volume_snapshot() -> Option { Some(VolumeSnapshot { enabled: false, diff --git a/tembo-operator/src/ingress.rs b/tembo-operator/src/ingress.rs index 9434e7b86..1d7f67128 100644 --- a/tembo-operator/src/ingress.rs +++ b/tembo-operator/src/ingress.rs @@ -289,12 +289,7 @@ pub async fn reconcile_postgres_ing_route_tcp( } fn generate_ip_allow_list_middleware_tcp(cdb: &CoreDB) -> MiddlewareTCP { - let source_range = match cdb.spec.ip_allow_list.clone() { - None => { - vec![] - } - Some(ips) => ips, - }; + let source_range = cdb.spec.ip_allow_list.clone().unwrap_or_default(); let mut valid_ips = valid_cidrs(&source_range); diff --git a/tembo-operator/src/network_policies.rs b/tembo-operator/src/network_policies.rs index 4d207a020..ea41a3a23 100644 --- a/tembo-operator/src/network_policies.rs +++ b/tembo-operator/src/network_policies.rs @@ -79,6 +79,50 @@ pub async fn reconcile_network_policies(client: Client, namespace: &str) -> Resu }); apply_network_policy(namespace, &np_api, allow_dns).await?; + let allow_node_local_dns = serde_json::json!({ + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "name": "allow-egress-to-node-local-dns", + "namespace": format!("{namespace}"), + }, + "spec": { + "podSelector": {}, + "policyTypes": [ + "Egress" + ], + "egress": [ + { + "to": [ + { + "podSelector": { + "matchLabels": { + "k8s-app": "node-local-dns" + } + }, + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "kube-system" + } + } + } + ], + "ports": [ + { + "protocol": "UDP", + "port": 53 + }, + { + "protocol": "TCP", + "port": 53 + } + ] + } + ] + } + }); + apply_network_policy(namespace, &np_api, allow_node_local_dns).await?; + // Namespaces that should be allowed to access an instance namespace let allow_system_ingress = serde_json::json!({ "apiVersion": "networking.k8s.io/v1", diff --git a/tembo-operator/src/trunk.rs b/tembo-operator/src/trunk.rs index 30ffcdc50..2aa85f492 100644 --- a/tembo-operator/src/trunk.rs +++ b/tembo-operator/src/trunk.rs @@ -707,7 +707,7 @@ mod tests { let result = is_semver(version); assert!(!result); - assert_eq!(is_semver("1.0"), false); + assert!(!is_semver("1.0")); } #[test]