From 4b892dd437d5571819d9002f1d14313c11c3029c Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 27 Nov 2025 11:13:06 +0100 Subject: [PATCH 01/14] add ssh key information --- .../src/crd/git_sync/mod.rs | 10 ++++ .../src/crd/git_sync/v1alpha1_impl.rs | 52 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index 92e9eab52..eb084cec8 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -63,5 +63,15 @@ pub mod versioned { /// [example]: DOCS_BASE_URL_PLACEHOLDER/airflow/usage-guide/mounting-dags#_example #[serde(default)] pub git_sync_conf: BTreeMap, + + /// The name of the Secret used for SSH access to the repository. + /// + /// The referenced Secret must include two fields: `key` and `knownHosts`. + /// + /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + pub ssh_secret: Option, + + #[serde(default = "GitSync::default_ssh_known_hosts")] + pub ssh_known_hosts: bool, } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index a29789ba8..3e4e38a51 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -9,7 +9,9 @@ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ builder::pod::{ - container::ContainerBuilder, resources::ResourceRequirementsBuilder, volume::VolumeBuilder, + container::ContainerBuilder, + resources::ResourceRequirementsBuilder, + volume::{VolumeBuilder, VolumeMountBuilder}, }, commons::product_image_selection::ResolvedProductImage, crd::git_sync::v1alpha1::GitSync, @@ -24,6 +26,8 @@ use crate::{ pub const CONTAINER_NAME_PREFIX: &str = "git-sync"; pub const VOLUME_NAME_PREFIX: &str = "content-from-git"; pub const MOUNT_PATH_PREFIX: &str = "/stackable/app/git"; +pub const SSH_VOLUME_NAME_PREFIX: &str = "ssh-keys-info"; +pub const SSH_MOUNT_PATH_PREFIX: &str = "/stackable/gitssh"; pub const GIT_SYNC_SAFE_DIR_OPTION: &str = "safe.directory"; pub const GIT_SYNC_ROOT_DIR: &str = "/tmp/git"; pub const GIT_SYNC_LINK: &str = "current"; @@ -58,6 +62,10 @@ impl GitSync { pub(crate) fn default_wait() -> Duration { Duration::from_secs(20) } + + pub(crate) fn default_ssh_known_hosts() -> bool { + true + } } /// Kubernetes resources generated from `GitSync` specifications which should be added to the Pod. @@ -77,6 +85,12 @@ pub struct GitSyncResources { /// Absolute paths to the Git contents in the mounted volumes pub git_content_folders: Vec, + + /// GitSync volumes containing the synchronized repository + pub git_ssh_volumes: Vec, + + /// Volume mounts for the GitSync volumes + pub git_ssh_volume_mounts: Vec, } impl GitSyncResources { @@ -120,6 +134,25 @@ impl GitSyncResources { "password", )); } + if git_sync.ssh_secret.is_some() { + env_vars.push(EnvVar { + name: "GITSYNC_SSH_KEY_FILE".to_owned(), + value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), + value_from: None, + }); + env_vars.push(EnvVar { + name: "GITSYNC_SSH_KNOWN_HOSTS_FILE".to_owned(), + value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/knownHosts").to_owned()), + value_from: None, + }); + } + // TODO should we leave to the defaults? + // env_vars.push(EnvVar { + // name: "GITSYNC_SSH_KNOWN_HOSTS".to_owned(), + // value: Some(git_sync.ssh_known_hosts.to_string()), + // value_from: None, + // }); + env_vars = insert_or_update_env_vars(&env_vars, extra_env_vars); let volume_name = format!("{VOLUME_NAME_PREFIX}-{i}"); @@ -186,6 +219,23 @@ impl GitSyncResources { .git_content_volume_mounts .push(git_content_volume_mount); resources.git_content_folders.push(git_content_folder); + + if let Some(get_ssh_secret) = &git_sync.ssh_secret { + let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); + let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); + + let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) + .with_secret(get_ssh_secret, false) + .build(); + resources.git_ssh_volumes.push(ssh_secret_volume); + + let ssh_secret_volume_mount = + VolumeMountBuilder::new(ssh_volume_name, ssh_mount_path).build(); + + resources + .git_ssh_volume_mounts + .push(ssh_secret_volume_mount); + } } Ok(resources) From a18aeb40cb4d7202309e5d3ceed57da0cd8c3cac Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 1 Dec 2025 11:57:34 +0100 Subject: [PATCH 02/14] remove ssh_known_hosts flag and git_ssh_volume_mounts --- .../src/crd/git_sync/mod.rs | 7 +- .../src/crd/git_sync/v1alpha1_impl.rs | 236 ++++++++++++++++-- 2 files changed, 218 insertions(+), 25 deletions(-) diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index eb084cec8..a3ac77f1e 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -20,7 +20,7 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GitSync { - /// The git repository URL that will be cloned, for example: `https://github.com/stackabletech/airflow-operator`. + /// The git repository URL that will be cloned, for example: `https://github.com/stackabletech/airflow-operator` or `ssh://git@github.com:stackable-airflow/dags.git`. pub repo: Url, /// The branch to clone; defaults to `main`. @@ -51,6 +51,7 @@ pub mod versioned { /// The referenced Secret must include two fields: `user` and `password`. /// The `password` field can either be an actual password (not recommended) or a GitHub token, /// as described in the git-sync [documentation]. + /// This cannot be provided if `ssh_secret` is also provided. /// /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual pub credentials_secret: Option, @@ -67,11 +68,9 @@ pub mod versioned { /// The name of the Secret used for SSH access to the repository. /// /// The referenced Secret must include two fields: `key` and `knownHosts`. + /// This cannot be provided if `credentials_secret` is also provided. /// /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual pub ssh_secret: Option, - - #[serde(default = "GitSync::default_ssh_known_hosts")] - pub ssh_known_hosts: bool, } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 3e4e38a51..985550d8d 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -62,10 +62,6 @@ impl GitSync { pub(crate) fn default_wait() -> Duration { Duration::from_secs(20) } - - pub(crate) fn default_ssh_known_hosts() -> bool { - true - } } /// Kubernetes resources generated from `GitSync` specifications which should be added to the Pod. @@ -88,9 +84,6 @@ pub struct GitSyncResources { /// GitSync volumes containing the synchronized repository pub git_ssh_volumes: Vec, - - /// Volume mounts for the GitSync volumes - pub git_ssh_volume_mounts: Vec, } impl GitSyncResources { @@ -146,12 +139,6 @@ impl GitSyncResources { value_from: None, }); } - // TODO should we leave to the defaults? - // env_vars.push(EnvVar { - // name: "GITSYNC_SSH_KNOWN_HOSTS".to_owned(), - // value: Some(git_sync.ssh_known_hosts.to_string()), - // value_from: None, - // }); env_vars = insert_or_update_env_vars(&env_vars, extra_env_vars); @@ -172,8 +159,18 @@ impl GitSyncResources { let mut git_sync_container_volume_mounts = vec![git_sync_root_volume_mount, log_volume_mount]; + git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); + if git_sync.ssh_secret.is_some() { + let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); + let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); + + let ssh_volume_mount = + VolumeMountBuilder::new(ssh_volume_name, ssh_mount_path).build(); + git_sync_container_volume_mounts.push(ssh_volume_mount); + } + let container = Self::create_git_sync_container( &format!("{CONTAINER_NAME_PREFIX}-{i}"), resolved_product_image, @@ -222,19 +219,11 @@ impl GitSyncResources { if let Some(get_ssh_secret) = &git_sync.ssh_secret { let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); - let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) .with_secret(get_ssh_secret, false) .build(); resources.git_ssh_volumes.push(ssh_secret_volume); - - let ssh_secret_volume_mount = - VolumeMountBuilder::new(ssh_volume_name, ssh_mount_path).build(); - - resources - .git_ssh_volume_mounts - .push(ssh_secret_volume_mount); } } @@ -926,4 +915,209 @@ name: content-from-git-2 .unwrap() ); } + + #[test] + fn test_git_sync_ssh() { + let git_sync_spec = r#" + # GitSync using SSH + - repo: ssh://git@github.com/stackabletech/repo.git + branch: trunk + gitFolder: "" + depth: 3 + wait: 1m + sshSecret: git-sync-ssh + gitSyncConf: + --rev: HEAD + --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt + "#; + + let git_syncs: Vec = yaml_from_str_singleton_map(git_sync_spec).unwrap(); + + let resolved_product_image = ResolvedProductImage { + image: "oci.stackable.tech/sdp/product:latest".to_string(), + app_version_label_value: "1.0.0-latest" + .parse() + .expect("static app version label is always valid"), + product_version: "1.0.0".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + }; + + let extra_env_vars = env_vars_from([("VAR1", "value1")]); + + let extra_volume_mounts = [VolumeMount { + name: "extra-volume".to_string(), + mount_path: "/mnt/extra-volume".to_string(), + ..VolumeMount::default() + }]; + + let git_sync_resources = GitSyncResources::new( + &git_syncs, + &resolved_product_image, + &extra_env_vars, + &extra_volume_mounts, + "log-volume", + &validate(default_container_log_config()).unwrap(), + ) + .unwrap(); + + assert!(git_sync_resources.is_git_sync_enabled()); + + assert_eq!(1, git_sync_resources.git_sync_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0 && exec > >(tee /stackable/log/git-sync-0/container.stdout.log) 2> >(tee /stackable/log/git-sync-0/container.stderr.log >&2) + + prepare_signal_handlers() + { + unset term_child_pid + unset term_kill_needed + trap 'handle_term_signal' TERM + } + + handle_term_signal() + { + if [ "${term_child_pid}" ]; then + kill -TERM "${term_child_pid}" 2>/dev/null + else + term_kill_needed="yes" + fi + } + + wait_for_termination() + { + set +e + term_child_pid=$1 + if [[ -v term_kill_needed ]]; then + kill -TERM "${term_child_pid}" 2>/dev/null + fi + wait ${term_child_pid} 2>/dev/null + trap - TERM + wait ${term_child_pid} 2>/dev/null + set -e + } + + prepare_signal_handlers + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=false --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git & + wait_for_termination $! +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_SSH_KEY_FILE + value: /stackable/gitssh-0/key +- name: GITSYNC_SSH_KNOWN_HOSTS_FILE + value: /stackable/gitssh-0/knownHosts +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0 +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +- mountPath: /stackable/gitssh-0 + name: ssh-keys-info-0 +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_containers.get(0)).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_sync_init_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0-init && exec > >(tee /stackable/log/git-sync-0-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-0-init/container.stderr.log >&2) + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=true --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_SSH_KEY_FILE + value: /stackable/gitssh-0/key +- name: GITSYNC_SSH_KNOWN_HOSTS_FILE + value: /stackable/gitssh-0/knownHosts +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0-init +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +- mountPath: /stackable/gitssh-0 + name: ssh-keys-info-0 +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_volumes.len()); + + assert_eq!( + "emptyDir: {} +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volumes.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_volume_mounts.len()); + + assert_eq!( + "mountPath: /stackable/app/git-0 +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_folders.len()); + + assert_eq!( + "/stackable/app/git-0/current/", + git_sync_resources + .git_content_folders_as_string() + .first() + .unwrap() + ); + + assert_eq!(1, git_sync_resources.git_ssh_volumes.len()); + + assert_eq!( + "name: ssh-keys-info-0 +secret: + optional: false + secretName: git-sync-ssh +", + serde_yaml::to_string(&git_sync_resources.git_ssh_volumes.first()).unwrap() + ); + } } From bed929e7da32894a783d585a952da502890060ee Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 1 Dec 2025 12:36:33 +0100 Subject: [PATCH 03/14] pre-commit fix --- crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 985550d8d..2e9d8fcbd 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -1035,7 +1035,7 @@ volumeMounts: - mountPath: /stackable/gitssh-0 name: ssh-keys-info-0 "#, - serde_yaml::to_string(&git_sync_resources.git_sync_containers.get(0)).unwrap() + serde_yaml::to_string(&git_sync_resources.git_sync_containers.first()).unwrap() ); assert_eq!(1, git_sync_resources.git_sync_init_containers.len()); From 843820ca7de0ba030610cc0e803a7a524d4f6993 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 1 Dec 2025 14:08:51 +0100 Subject: [PATCH 04/14] added check for multiple credential types --- .../stackable-operator/src/crd/git_sync/v1alpha1_impl.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 2e9d8fcbd..e0f67fd4b 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -44,6 +44,9 @@ pub enum Error { AddVolumeMount { source: crate::builder::pod::container::Error, }, + + #[snafu(display("failed to declare unique credentials"))] + MultipleCredentials, } impl GitSync { @@ -114,6 +117,11 @@ impl GitSyncResources { let mut resources = GitSyncResources::default(); for (i, git_sync) in git_syncs.iter().enumerate() { + if git_sync.credentials_secret.is_some() && git_sync.ssh_secret.is_some() { + // Gitsync will not allow the declaration of both ssh-key and password/token credentials + return Err(Error::MultipleCredentials); + } + let mut env_vars = vec![]; if let Some(git_credentials_secret) = &git_sync.credentials_secret { env_vars.push(GitSyncResources::env_var_from_secret( From 3ab7a5f9aeb594a3bceb3220f970bccb7aac4c1a Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 1 Dec 2025 14:33:59 +0100 Subject: [PATCH 05/14] changelog --- crates/stackable-operator/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index a62911593..c9caf925d 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -13,12 +13,14 @@ All notable changes to this project will be documented in this file. - BREAKING: `ClusterResources` now requires the objects added to implement `DeepMerge`. This is very likely a stackable-operator internal change, but technically breaking ([#1118]). +- Add support for the SSH protocol for pulling git content ([#1121]). ### Removed - BREAKING: `ClusterResources` no longer derives `Eq` ([#1118]). [#1118]: https://github.com/stackabletech/operator-rs/pull/1118 +[#1121]: https://github.com/stackabletech/operator-rs/pull/1121 ## [0.100.3] - 2025-10-31 From 3bcde9083bc6c55f227d43211f14e5e8c0279334 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 4 Dec 2025 09:45:18 +0100 Subject: [PATCH 06/14] add gitsync to dummy cluster --- .../stackable-operator/crds/DummyCluster.yaml | 72 +++++++++++++++++++ crates/xtask/src/crd/dummy.rs | 2 + 2 files changed, 74 insertions(+) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index c96b4b557..8e95729fa 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -75,6 +75,77 @@ spec: domainName: description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address type: string + gitSync: + properties: + branch: + default: main + description: |- + The branch to clone; defaults to `main`. + + Since git-sync v4.x.x this field is mapped to the flag `--ref`. + type: string + credentialsSecret: + description: |- + The name of the Secret used to access the repository if it is not public. + + The referenced Secret must include two fields: `user` and `password`. + The `password` field can either be an actual password (not recommended) or a GitHub token, + as described in the git-sync [documentation]. + This cannot be provided if `ssh_secret` is also provided. + + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + nullable: true + type: string + depth: + default: 1 + description: The depth of syncing, i.e. the number of commits to clone; defaults to 1. + format: uint32 + minimum: 0.0 + type: integer + gitFolder: + default: / + description: |- + Location in the Git repository containing the resource; defaults to the root folder. + + It can optionally start with `/`, however, no trailing slash is recommended. + An empty string (``) or slash (`/`) corresponds to the root folder in Git. + type: string + gitSyncConf: + additionalProperties: + type: string + default: {} + description: |- + A map of optional configuration settings that are listed in the git-sync [documentation]. + + Also read the git-sync [example] in our documentation. These settings are not verified. + + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + [example]: https://docs.stackable.tech/home/nightly/airflow/usage-guide/mounting-dags#_example + type: object + repo: + description: 'The git repository URL that will be cloned, for example: `https://github.com/stackabletech/airflow-operator` or `ssh://git@github.com:stackable-airflow/dags.git`.' + format: uri + type: string + sshSecret: + description: |- + The name of the Secret used for SSH access to the repository. + + The referenced Secret must include two fields: `key` and `knownHosts`. + This cannot be provided if `credentials_secret` is also provided. + + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + nullable: true + type: string + wait: + default: 20s + description: |- + The synchronization interval, e.g. `20s` or `5m`; defaults to `20s`. + + Since git-sync v4.x.x this field is mapped to the flag `--period`. + type: string + required: + - repo + type: object hostName: type: string kerberosRealmName: @@ -1402,6 +1473,7 @@ spec: - clientAuthenticationDetails - clusterOperation - domainName + - gitSync - hostName - kerberosRealmName - opaConfig diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index ac65c4f60..28a0c3f91 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{JvmHeapLimits, Resources}, config::fragment::Fragment, + crd::git_sync::v1alpha1::GitSync, deep_merger::ObjectOverrides, kube::CustomResource, role_utils::Role, @@ -48,6 +49,7 @@ pub mod versioned { secret_class_volume: stackable_operator::commons::secret_class::SecretClassVolume, secret_reference: stackable_operator::shared::secret::SecretReference, tls_client_details: stackable_operator::commons::tls_verification::TlsClientDetails, + git_sync: GitSync, #[serde(default)] pub object_overrides: ObjectOverrides, From 0e024d4ddc10ab947d53e2d31484f3816d26929d Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Mon, 8 Dec 2025 12:12:09 +0100 Subject: [PATCH 07/14] replace fields with enum --- .../stackable-operator/crds/DummyCluster.yaml | 9 ++-- .../src/crd/git_sync/mod.rs | 48 ++++++++++++------- .../src/crd/git_sync/v1alpha1_impl.rs | 23 ++++----- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 8e95729fa..84be1ba8d 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -76,6 +76,11 @@ spec: description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address type: string gitSync: + anyOf: + - required: + - credentialsSecret + - required: + - sshSecret properties: branch: default: main @@ -91,10 +96,8 @@ spec: The referenced Secret must include two fields: `user` and `password`. The `password` field can either be an actual password (not recommended) or a GitHub token, as described in the git-sync [documentation]. - This cannot be provided if `ssh_secret` is also provided. [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - nullable: true type: string depth: default: 1 @@ -131,10 +134,8 @@ spec: The name of the Secret used for SSH access to the repository. The referenced Secret must include two fields: `key` and `knownHosts`. - This cannot be provided if `credentials_secret` is also provided. [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - nullable: true type: string wait: default: 20s diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index a3ac77f1e..b17544224 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -46,16 +46,6 @@ pub mod versioned { #[serde(default = "GitSync::default_wait")] pub wait: Duration, - /// The name of the Secret used to access the repository if it is not public. - /// - /// The referenced Secret must include two fields: `user` and `password`. - /// The `password` field can either be an actual password (not recommended) or a GitHub token, - /// as described in the git-sync [documentation]. - /// This cannot be provided if `ssh_secret` is also provided. - /// - /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - pub credentials_secret: Option, - /// A map of optional configuration settings that are listed in the git-sync [documentation]. /// /// Also read the git-sync [example] in our documentation. These settings are not verified. @@ -65,12 +55,36 @@ pub mod versioned { #[serde(default)] pub git_sync_conf: BTreeMap, - /// The name of the Secret used for SSH access to the repository. - /// - /// The referenced Secret must include two fields: `key` and `knownHosts`. - /// This cannot be provided if `credentials_secret` is also provided. - /// - /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - pub ssh_secret: Option, + #[serde(flatten)] + pub access_secret: Option, + } + + #[derive(strum::Display, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(untagged)] + #[serde(rename_all = "camelCase")] + #[schemars(rename_all = "camelCase")] + pub enum AccessSecret { + Credentials { + /// The name of the Secret used to access the repository if it is not public. + /// + /// The referenced Secret must include two fields: `user` and `password`. + /// The `password` field can either be an actual password (not recommended) or a GitHub token, + /// as described in the git-sync [documentation]. + /// + /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + #[serde(rename = "credentialsSecret")] + #[schemars(rename = "credentialsSecret")] + credentials_secret: String, + }, + Ssh { + /// The name of the Secret used for SSH access to the repository. + /// + /// The referenced Secret must include two fields: `key` and `knownHosts`. + /// + /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + #[serde(rename = "sshSecret")] + #[schemars(rename = "sshSecret")] + ssh_secret: String, + }, } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index e0f67fd4b..61c04cb37 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -14,7 +14,7 @@ use crate::{ volume::{VolumeBuilder, VolumeMountBuilder}, }, commons::product_image_selection::ResolvedProductImage, - crd::git_sync::v1alpha1::GitSync, + crd::git_sync::v1alpha1::{AccessSecret, GitSync}, product_config_utils::insert_or_update_env_vars, product_logging::{ framework::capture_shell_output, @@ -117,25 +117,22 @@ impl GitSyncResources { let mut resources = GitSyncResources::default(); for (i, git_sync) in git_syncs.iter().enumerate() { - if git_sync.credentials_secret.is_some() && git_sync.ssh_secret.is_some() { - // Gitsync will not allow the declaration of both ssh-key and password/token credentials - return Err(Error::MultipleCredentials); - } - let mut env_vars = vec![]; - if let Some(git_credentials_secret) = &git_sync.credentials_secret { + + if let Some(AccessSecret::Credentials { credentials_secret }) = &git_sync.access_secret + { env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_USERNAME", - git_credentials_secret, + credentials_secret, "user", )); env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_PASSWORD", - git_credentials_secret, + credentials_secret, "password", )); } - if git_sync.ssh_secret.is_some() { + if matches!(git_sync.access_secret, Some(AccessSecret::Ssh { .. })) { env_vars.push(EnvVar { name: "GITSYNC_SSH_KEY_FILE".to_owned(), value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), @@ -170,7 +167,7 @@ impl GitSyncResources { git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); - if git_sync.ssh_secret.is_some() { + if matches!(git_sync.access_secret, Some(AccessSecret::Ssh { .. })) { let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); @@ -225,11 +222,11 @@ impl GitSyncResources { .push(git_content_volume_mount); resources.git_content_folders.push(git_content_folder); - if let Some(get_ssh_secret) = &git_sync.ssh_secret { + if let Some(AccessSecret::Ssh { ssh_secret }) = &git_sync.access_secret { let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) - .with_secret(get_ssh_secret, false) + .with_secret(ssh_secret, false) .build(); resources.git_ssh_volumes.push(ssh_secret_volume); } From f31b96543981f89b737c06ea324a7d18b3d65d98 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 18 Dec 2025 10:36:59 +0100 Subject: [PATCH 08/14] flatten enum and re-name fields --- .../stackable-operator/crds/DummyCluster.yaml | 47 ++++++++++--------- .../src/crd/git_sync/mod.rs | 22 ++++----- .../src/crd/git_sync/v1alpha1_impl.rs | 27 +++++++---- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 84be1ba8d..195a11ccd 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -76,11 +76,6 @@ spec: description: A validated domain name type conforming to RFC 1123, so e.g. not an IP address type: string gitSync: - anyOf: - - required: - - credentialsSecret - - required: - - sshSecret properties: branch: default: main @@ -89,16 +84,34 @@ spec: Since git-sync v4.x.x this field is mapped to the flag `--ref`. type: string - credentialsSecret: - description: |- - The name of the Secret used to access the repository if it is not public. + credentials: + anyOf: + - required: + - basicAuthSecretName + - required: + - sshPrivateKeySecretName + description: An optional secret used for git access. + nullable: true + properties: + basicAuthSecretName: + description: |- + The name of the Secret used to access the repository via Basic Authentication if it is not public. - The referenced Secret must include two fields: `user` and `password`. - The `password` field can either be an actual password (not recommended) or a GitHub token, - as described in the git-sync [documentation]. + The referenced Secret must include two fields: `user` and `password`. + The `password` field can either be an actual password (not recommended) or a GitHub token, + as described in the git-sync [documentation]. - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string + sshPrivateKeySecretName: + description: |- + The name of the Secret used for SSH access to the repository. + + The referenced Secret must include two fields: `key` and `knownHosts`. + + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string + type: object depth: default: 1 description: The depth of syncing, i.e. the number of commits to clone; defaults to 1. @@ -129,14 +142,6 @@ spec: description: 'The git repository URL that will be cloned, for example: `https://github.com/stackabletech/airflow-operator` or `ssh://git@github.com:stackable-airflow/dags.git`.' format: uri type: string - sshSecret: - description: |- - The name of the Secret used for SSH access to the repository. - - The referenced Secret must include two fields: `key` and `knownHosts`. - - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string wait: default: 20s description: |- diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index b17544224..4a575219b 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -55,26 +55,26 @@ pub mod versioned { #[serde(default)] pub git_sync_conf: BTreeMap, - #[serde(flatten)] - pub access_secret: Option, + /// An optional secret used for git access. + pub credentials: Option, } #[derive(strum::Display, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(untagged)] #[serde(rename_all = "camelCase")] #[schemars(rename_all = "camelCase")] - pub enum AccessSecret { - Credentials { - /// The name of the Secret used to access the repository if it is not public. + pub enum Credentials { + BasicAuth { + /// The name of the Secret used to access the repository via Basic Authentication if it is not public. /// /// The referenced Secret must include two fields: `user` and `password`. /// The `password` field can either be an actual password (not recommended) or a GitHub token, /// as described in the git-sync [documentation]. /// /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - #[serde(rename = "credentialsSecret")] - #[schemars(rename = "credentialsSecret")] - credentials_secret: String, + #[serde(rename = "basicAuthSecretName")] + #[schemars(rename = "basicAuthSecretName")] + basic_auth_secret_name: String, }, Ssh { /// The name of the Secret used for SSH access to the repository. @@ -82,9 +82,9 @@ pub mod versioned { /// The referenced Secret must include two fields: `key` and `knownHosts`. /// /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - #[serde(rename = "sshSecret")] - #[schemars(rename = "sshSecret")] - ssh_secret: String, + #[serde(rename = "sshPrivateKeySecretName")] + #[schemars(rename = "sshPrivateKeySecretName")] + ssh_private_key_secret_name: String, }, } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 61c04cb37..5fffba36c 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -14,7 +14,7 @@ use crate::{ volume::{VolumeBuilder, VolumeMountBuilder}, }, commons::product_image_selection::ResolvedProductImage, - crd::git_sync::v1alpha1::{AccessSecret, GitSync}, + crd::git_sync::v1alpha1::{Credentials, GitSync}, product_config_utils::insert_or_update_env_vars, product_logging::{ framework::capture_shell_output, @@ -119,20 +119,22 @@ impl GitSyncResources { for (i, git_sync) in git_syncs.iter().enumerate() { let mut env_vars = vec![]; - if let Some(AccessSecret::Credentials { credentials_secret }) = &git_sync.access_secret + if let Some(Credentials::BasicAuth { + basic_auth_secret_name, + }) = &git_sync.credentials { env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_USERNAME", - credentials_secret, + basic_auth_secret_name, "user", )); env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_PASSWORD", - credentials_secret, + basic_auth_secret_name, "password", )); } - if matches!(git_sync.access_secret, Some(AccessSecret::Ssh { .. })) { + if matches!(git_sync.credentials, Some(Credentials::Ssh { .. })) { env_vars.push(EnvVar { name: "GITSYNC_SSH_KEY_FILE".to_owned(), value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), @@ -167,7 +169,7 @@ impl GitSyncResources { git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); - if matches!(git_sync.access_secret, Some(AccessSecret::Ssh { .. })) { + if matches!(git_sync.credentials, Some(Credentials::Ssh { .. })) { let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); @@ -222,11 +224,14 @@ impl GitSyncResources { .push(git_content_volume_mount); resources.git_content_folders.push(git_content_folder); - if let Some(AccessSecret::Ssh { ssh_secret }) = &git_sync.access_secret { + if let Some(Credentials::Ssh { + ssh_private_key_secret_name, + }) = &git_sync.credentials + { let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) - .with_secret(ssh_secret, false) + .with_secret(ssh_private_key_secret_name, false) .build(); resources.git_ssh_volumes.push(ssh_secret_volume); } @@ -460,7 +465,8 @@ mod tests { gitFolder: "" depth: 3 wait: 1m - credentialsSecret: git-credentials + credentials: + basicAuthSecretName: git-credentials gitSyncConf: --rev: HEAD --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt @@ -930,7 +936,8 @@ name: content-from-git-2 gitFolder: "" depth: 3 wait: 1m - sshSecret: git-sync-ssh + credentials: + sshPrivateKeySecretName: git-sync-ssh gitSyncConf: --rev: HEAD --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt From 6c26212ed00cdd6a7ebe6ac555bb5d1f92ad8d2b Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:05:43 +0100 Subject: [PATCH 09/14] Update crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs Co-authored-by: Sebastian Bernauer --- crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 5fffba36c..f3253058a 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -44,9 +44,6 @@ pub enum Error { AddVolumeMount { source: crate::builder::pod::container::Error, }, - - #[snafu(display("failed to declare unique credentials"))] - MultipleCredentials, } impl GitSync { From 41ba13c0e4dcde44cb6881d8aa523ad2ba143aeb Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy <1712947+adwk67@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:05:50 +0100 Subject: [PATCH 10/14] Update crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs Co-authored-by: Sebastian Bernauer --- crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index f3253058a..ba9ca109b 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -131,7 +131,7 @@ impl GitSyncResources { "password", )); } - if matches!(git_sync.credentials, Some(Credentials::Ssh { .. })) { + if let Some(Credentials::Ssh { .. }) = git_sync.credentials { env_vars.push(EnvVar { name: "GITSYNC_SSH_KEY_FILE".to_owned(), value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), From d14cc6af7d963dd668ca6baee1a85910e4026205 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 18 Dec 2025 12:11:36 +0100 Subject: [PATCH 11/14] make enum untagged --- .../stackable-operator/crds/DummyCluster.yaml | 48 +++++++++++-------- .../src/crd/git_sync/mod.rs | 1 - 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 195a11ccd..0a1114c14 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -85,32 +85,42 @@ spec: Since git-sync v4.x.x this field is mapped to the flag `--ref`. type: string credentials: - anyOf: - - required: - - basicAuthSecretName - - required: - - sshPrivateKeySecretName description: An optional secret used for git access. nullable: true + oneOf: + - required: + - basicAuth + - required: + - ssh properties: - basicAuthSecretName: - description: |- - The name of the Secret used to access the repository via Basic Authentication if it is not public. + basicAuth: + properties: + basicAuthSecretName: + description: |- + The name of the Secret used to access the repository via Basic Authentication if it is not public. - The referenced Secret must include two fields: `user` and `password`. - The `password` field can either be an actual password (not recommended) or a GitHub token, - as described in the git-sync [documentation]. + The referenced Secret must include two fields: `user` and `password`. + The `password` field can either be an actual password (not recommended) or a GitHub token, + as described in the git-sync [documentation]. - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string - sshPrivateKeySecretName: - description: |- - The name of the Secret used for SSH access to the repository. + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string + required: + - basicAuthSecretName + type: object + ssh: + properties: + sshPrivateKeySecretName: + description: |- + The name of the Secret used for SSH access to the repository. - The referenced Secret must include two fields: `key` and `knownHosts`. + The referenced Secret must include two fields: `key` and `knownHosts`. - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string + required: + - sshPrivateKeySecretName + type: object type: object depth: default: 1 diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index 4a575219b..fb6dd8f5d 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -60,7 +60,6 @@ pub mod versioned { } #[derive(strum::Display, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] - #[serde(untagged)] #[serde(rename_all = "camelCase")] #[schemars(rename_all = "camelCase")] pub enum Credentials { From fbcb301669afb8e43500575479e84ec7a55e0dbb Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 18 Dec 2025 12:45:37 +0100 Subject: [PATCH 12/14] make enum untagged: corrected --- .../stackable-operator/crds/DummyCluster.yaml | 42 +++++++------------ .../src/crd/git_sync/mod.rs | 36 +++++++--------- .../src/crd/git_sync/v1alpha1_impl.rs | 14 +++---- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/crates/stackable-operator/crds/DummyCluster.yaml b/crates/stackable-operator/crds/DummyCluster.yaml index 0a1114c14..201e1fc66 100644 --- a/crates/stackable-operator/crds/DummyCluster.yaml +++ b/crates/stackable-operator/crds/DummyCluster.yaml @@ -89,38 +89,28 @@ spec: nullable: true oneOf: - required: - - basicAuth + - basicAuthSecretName - required: - - ssh + - sshPrivateKeySecretName properties: - basicAuth: - properties: - basicAuthSecretName: - description: |- - The name of the Secret used to access the repository via Basic Authentication if it is not public. + basicAuthSecretName: + description: |- + The name of the Secret used to access the repository via Basic Authentication if it is not public. - The referenced Secret must include two fields: `user` and `password`. - The `password` field can either be an actual password (not recommended) or a GitHub token, - as described in the git-sync [documentation]. + The referenced Secret must include two fields: `user` and `password`. + The `password` field can either be an actual password (not recommended) or a GitHub token, + as described in the git-sync [documentation]. - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string - required: - - basicAuthSecretName - type: object - ssh: - properties: - sshPrivateKeySecretName: - description: |- - The name of the Secret used for SSH access to the repository. + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string + sshPrivateKeySecretName: + description: |- + The name of the Secret used for SSH access to the repository. - The referenced Secret must include two fields: `key` and `knownHosts`. + The referenced Secret must include two fields: `key` and `knownHosts`. - [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - type: string - required: - - sshPrivateKeySecretName - type: object + [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + type: string type: object depth: default: 1 diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index fb6dd8f5d..8f0c8086d 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -63,27 +63,19 @@ pub mod versioned { #[serde(rename_all = "camelCase")] #[schemars(rename_all = "camelCase")] pub enum Credentials { - BasicAuth { - /// The name of the Secret used to access the repository via Basic Authentication if it is not public. - /// - /// The referenced Secret must include two fields: `user` and `password`. - /// The `password` field can either be an actual password (not recommended) or a GitHub token, - /// as described in the git-sync [documentation]. - /// - /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - #[serde(rename = "basicAuthSecretName")] - #[schemars(rename = "basicAuthSecretName")] - basic_auth_secret_name: String, - }, - Ssh { - /// The name of the Secret used for SSH access to the repository. - /// - /// The referenced Secret must include two fields: `key` and `knownHosts`. - /// - /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual - #[serde(rename = "sshPrivateKeySecretName")] - #[schemars(rename = "sshPrivateKeySecretName")] - ssh_private_key_secret_name: String, - }, + /// The name of the Secret used to access the repository via Basic Authentication if it is not public. + /// + /// The referenced Secret must include two fields: `user` and `password`. + /// The `password` field can either be an actual password (not recommended) or a GitHub token, + /// as described in the git-sync [documentation]. + /// + /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + BasicAuthSecretName(String), + /// The name of the Secret used for SSH access to the repository. + /// + /// The referenced Secret must include two fields: `key` and `knownHosts`. + /// + /// [documentation]: https://github.com/kubernetes/git-sync/tree/v4.2.4?tab=readme-ov-file#manual + SshPrivateKeySecretName(String), } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index ba9ca109b..3d25d3c22 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -116,9 +116,8 @@ impl GitSyncResources { for (i, git_sync) in git_syncs.iter().enumerate() { let mut env_vars = vec![]; - if let Some(Credentials::BasicAuth { - basic_auth_secret_name, - }) = &git_sync.credentials + if let Some(Credentials::BasicAuthSecretName(basic_auth_secret_name)) = + &git_sync.credentials { env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_USERNAME", @@ -131,7 +130,7 @@ impl GitSyncResources { "password", )); } - if let Some(Credentials::Ssh { .. }) = git_sync.credentials { + if let Some(Credentials::SshPrivateKeySecretName { .. }) = git_sync.credentials { env_vars.push(EnvVar { name: "GITSYNC_SSH_KEY_FILE".to_owned(), value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), @@ -166,7 +165,7 @@ impl GitSyncResources { git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); - if matches!(git_sync.credentials, Some(Credentials::Ssh { .. })) { + if let Some(Credentials::SshPrivateKeySecretName(_)) = git_sync.credentials { let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); @@ -221,9 +220,8 @@ impl GitSyncResources { .push(git_content_volume_mount); resources.git_content_folders.push(git_content_folder); - if let Some(Credentials::Ssh { - ssh_private_key_secret_name, - }) = &git_sync.credentials + if let Some(Credentials::SshPrivateKeySecretName(ssh_private_key_secret_name)) = + &git_sync.credentials { let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); From ec980f4862a9f2c4529cc1058056c53440268ac9 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 18 Dec 2025 13:04:05 +0100 Subject: [PATCH 13/14] changelog: mark change as breaking --- crates/stackable-operator/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index 9275935fd..63bc1d173 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -13,7 +13,8 @@ All notable changes to this project will be documented in this file. - BREAKING: `ClusterResources` now requires the objects added to implement `DeepMerge`. This is very likely a stackable-operator internal change, but technically breaking ([#1118]). -- Add support for the SSH protocol for pulling git content ([#1121]). +- BREAKING: Add support for the SSH protocol for pulling git content ([#1121]). + This is a user-facing breaking change and should also be highlighted by operators using this functionality. - Depend on the patched version of kube-rs available at , ensuring the operators automatically benefit from the fixes ([#1124]). From 8b94f6a154f54eca2df9bba839e510ed45b99233 Mon Sep 17 00:00:00 2001 From: Andrew Kenworthy Date: Thu, 18 Dec 2025 16:35:15 +0100 Subject: [PATCH 14/14] wip: versioning --- .../src/crd/git_sync/mod.rs | 35 +- .../src/crd/git_sync/v1alpha1_impl.rs | 282 +--- .../src/crd/git_sync/v1alpha2_impl.rs | 1130 +++++++++++++++++ crates/xtask/src/crd/dummy.rs | 2 +- 4 files changed, 1166 insertions(+), 283 deletions(-) create mode 100644 crates/stackable-operator/src/crd/git_sync/v1alpha2_impl.rs diff --git a/crates/stackable-operator/src/crd/git_sync/mod.rs b/crates/stackable-operator/src/crd/git_sync/mod.rs index 8f0c8086d..511b30f9b 100644 --- a/crates/stackable-operator/src/crd/git_sync/mod.rs +++ b/crates/stackable-operator/src/crd/git_sync/mod.rs @@ -7,15 +7,19 @@ use serde::{Deserialize, Serialize}; use stackable_shared::time::Duration; use url::Url; -use crate::versioned::versioned; +use crate::{crd::git_sync::v1alpha2::Credentials, versioned::versioned}; mod v1alpha1_impl; +mod v1alpha2_impl; -#[versioned(version(name = "v1alpha1"))] +#[versioned(version(name = "v1alpha1"), version(name = "v1alpha2"))] pub mod versioned { pub mod v1alpha1 { pub use v1alpha1_impl::{Error, GitSyncResources}; } + pub mod v1alpha2 { + pub use v1alpha2_impl::{Error, GitSyncResources}; + } #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] @@ -26,24 +30,24 @@ pub mod versioned { /// The branch to clone; defaults to `main`. /// /// Since git-sync v4.x.x this field is mapped to the flag `--ref`. - #[serde(default = "GitSync::default_branch")] + #[serde(default = "v1alpha2::GitSync::default_branch")] pub branch: String, /// Location in the Git repository containing the resource; defaults to the root folder. /// /// It can optionally start with `/`, however, no trailing slash is recommended. /// An empty string (``) or slash (`/`) corresponds to the root folder in Git. - #[serde(default = "GitSync::default_git_folder")] + #[serde(default = "v1alpha2::GitSync::default_git_folder")] pub git_folder: PathBuf, /// The depth of syncing, i.e. the number of commits to clone; defaults to 1. - #[serde(default = "GitSync::default_depth")] + #[serde(default = "v1alpha2::GitSync::default_depth")] pub depth: u32, /// The synchronization interval, e.g. `20s` or `5m`; defaults to `20s`. /// /// Since git-sync v4.x.x this field is mapped to the flag `--period`. - #[serde(default = "GitSync::default_wait")] + #[serde(default = "v1alpha2::GitSync::default_wait")] pub wait: Duration, /// A map of optional configuration settings that are listed in the git-sync [documentation]. @@ -56,6 +60,13 @@ pub mod versioned { pub git_sync_conf: BTreeMap, /// An optional secret used for git access. + #[versioned(changed( + since = "v1alpha2", + from_name = "credentials_secret", + from_type = "Option", + upgrade_with = credentials_secret_to_basic_auth, + downgrade_with = credentials_to_secret + ))] pub credentials: Option, } @@ -79,3 +90,15 @@ pub mod versioned { SshPrivateKeySecretName(String), } } + +pub fn credentials_to_secret(input: Option) -> Option { + if let Some(Credentials::BasicAuthSecretName(credentials_secret)) = input { + Some(credentials_secret) + } else { + None + } +} + +pub fn credentials_secret_to_basic_auth(input: Option) -> Option { + input.map(Credentials::BasicAuthSecretName) +} diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs index 3d25d3c22..7395290e1 100644 --- a/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs @@ -4,17 +4,14 @@ use k8s_openapi::api::core::v1::{ Container, EmptyDirVolumeSource, EnvVar, EnvVarSource, SecretKeySelector, Volume, VolumeMount, }; use snafu::{ResultExt, Snafu}; -use stackable_shared::time::Duration; use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ builder::pod::{ - container::ContainerBuilder, - resources::ResourceRequirementsBuilder, - volume::{VolumeBuilder, VolumeMountBuilder}, + container::ContainerBuilder, resources::ResourceRequirementsBuilder, volume::VolumeBuilder, }, commons::product_image_selection::ResolvedProductImage, - crd::git_sync::v1alpha1::{Credentials, GitSync}, + crd::git_sync::v1alpha1::GitSync, product_config_utils::insert_or_update_env_vars, product_logging::{ framework::capture_shell_output, @@ -26,8 +23,6 @@ use crate::{ pub const CONTAINER_NAME_PREFIX: &str = "git-sync"; pub const VOLUME_NAME_PREFIX: &str = "content-from-git"; pub const MOUNT_PATH_PREFIX: &str = "/stackable/app/git"; -pub const SSH_VOLUME_NAME_PREFIX: &str = "ssh-keys-info"; -pub const SSH_MOUNT_PATH_PREFIX: &str = "/stackable/gitssh"; pub const GIT_SYNC_SAFE_DIR_OPTION: &str = "safe.directory"; pub const GIT_SYNC_ROOT_DIR: &str = "/tmp/git"; pub const GIT_SYNC_LINK: &str = "current"; @@ -46,24 +41,6 @@ pub enum Error { }, } -impl GitSync { - pub(crate) fn default_branch() -> String { - "main".to_string() - } - - pub(crate) fn default_git_folder() -> PathBuf { - PathBuf::from("/") - } - - pub(crate) fn default_depth() -> u32 { - 1 - } - - pub(crate) fn default_wait() -> Duration { - Duration::from_secs(20) - } -} - /// Kubernetes resources generated from `GitSync` specifications which should be added to the Pod. #[derive(Default)] pub struct GitSyncResources { @@ -81,9 +58,6 @@ pub struct GitSyncResources { /// Absolute paths to the Git contents in the mounted volumes pub git_content_folders: Vec, - - /// GitSync volumes containing the synchronized repository - pub git_ssh_volumes: Vec, } impl GitSyncResources { @@ -115,34 +89,18 @@ impl GitSyncResources { for (i, git_sync) in git_syncs.iter().enumerate() { let mut env_vars = vec![]; - - if let Some(Credentials::BasicAuthSecretName(basic_auth_secret_name)) = - &git_sync.credentials - { + if let Some(git_credentials_secret) = &git_sync.credentials_secret { env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_USERNAME", - basic_auth_secret_name, + git_credentials_secret, "user", )); env_vars.push(GitSyncResources::env_var_from_secret( "GITSYNC_PASSWORD", - basic_auth_secret_name, + git_credentials_secret, "password", )); } - if let Some(Credentials::SshPrivateKeySecretName { .. }) = git_sync.credentials { - env_vars.push(EnvVar { - name: "GITSYNC_SSH_KEY_FILE".to_owned(), - value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), - value_from: None, - }); - env_vars.push(EnvVar { - name: "GITSYNC_SSH_KNOWN_HOSTS_FILE".to_owned(), - value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/knownHosts").to_owned()), - value_from: None, - }); - } - env_vars = insert_or_update_env_vars(&env_vars, extra_env_vars); let volume_name = format!("{VOLUME_NAME_PREFIX}-{i}"); @@ -162,18 +120,8 @@ impl GitSyncResources { let mut git_sync_container_volume_mounts = vec![git_sync_root_volume_mount, log_volume_mount]; - git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); - if let Some(Credentials::SshPrivateKeySecretName(_)) = git_sync.credentials { - let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); - let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); - - let ssh_volume_mount = - VolumeMountBuilder::new(ssh_volume_name, ssh_mount_path).build(); - git_sync_container_volume_mounts.push(ssh_volume_mount); - } - let container = Self::create_git_sync_container( &format!("{CONTAINER_NAME_PREFIX}-{i}"), resolved_product_image, @@ -219,17 +167,6 @@ impl GitSyncResources { .git_content_volume_mounts .push(git_content_volume_mount); resources.git_content_folders.push(git_content_folder); - - if let Some(Credentials::SshPrivateKeySecretName(ssh_private_key_secret_name)) = - &git_sync.credentials - { - let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); - - let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) - .with_secret(ssh_private_key_secret_name, false) - .build(); - resources.git_ssh_volumes.push(ssh_secret_volume); - } } Ok(resources) @@ -460,8 +397,7 @@ mod tests { gitFolder: "" depth: 3 wait: 1m - credentials: - basicAuthSecretName: git-credentials + credentialsSecret: git-credentials gitSyncConf: --rev: HEAD --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt @@ -921,210 +857,4 @@ name: content-from-git-2 .unwrap() ); } - - #[test] - fn test_git_sync_ssh() { - let git_sync_spec = r#" - # GitSync using SSH - - repo: ssh://git@github.com/stackabletech/repo.git - branch: trunk - gitFolder: "" - depth: 3 - wait: 1m - credentials: - sshPrivateKeySecretName: git-sync-ssh - gitSyncConf: - --rev: HEAD - --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt - "#; - - let git_syncs: Vec = yaml_from_str_singleton_map(git_sync_spec).unwrap(); - - let resolved_product_image = ResolvedProductImage { - image: "oci.stackable.tech/sdp/product:latest".to_string(), - app_version_label_value: "1.0.0-latest" - .parse() - .expect("static app version label is always valid"), - product_version: "1.0.0".to_string(), - image_pull_policy: "Always".to_string(), - pull_secrets: None, - }; - - let extra_env_vars = env_vars_from([("VAR1", "value1")]); - - let extra_volume_mounts = [VolumeMount { - name: "extra-volume".to_string(), - mount_path: "/mnt/extra-volume".to_string(), - ..VolumeMount::default() - }]; - - let git_sync_resources = GitSyncResources::new( - &git_syncs, - &resolved_product_image, - &extra_env_vars, - &extra_volume_mounts, - "log-volume", - &validate(default_container_log_config()).unwrap(), - ) - .unwrap(); - - assert!(git_sync_resources.is_git_sync_enabled()); - - assert_eq!(1, git_sync_resources.git_sync_containers.len()); - - assert_eq!( - r#"args: -- |- - mkdir --parents /stackable/log/git-sync-0 && exec > >(tee /stackable/log/git-sync-0/container.stdout.log) 2> >(tee /stackable/log/git-sync-0/container.stderr.log >&2) - - prepare_signal_handlers() - { - unset term_child_pid - unset term_kill_needed - trap 'handle_term_signal' TERM - } - - handle_term_signal() - { - if [ "${term_child_pid}" ]; then - kill -TERM "${term_child_pid}" 2>/dev/null - else - term_kill_needed="yes" - fi - } - - wait_for_termination() - { - set +e - term_child_pid=$1 - if [[ -v term_kill_needed ]]; then - kill -TERM "${term_child_pid}" 2>/dev/null - fi - wait ${term_child_pid} 2>/dev/null - trap - TERM - wait ${term_child_pid} 2>/dev/null - set -e - } - - prepare_signal_handlers - /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=false --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git & - wait_for_termination $! -command: -- /bin/bash -- -x -- -euo -- pipefail -- -c -env: -- name: GITSYNC_SSH_KEY_FILE - value: /stackable/gitssh-0/key -- name: GITSYNC_SSH_KNOWN_HOSTS_FILE - value: /stackable/gitssh-0/knownHosts -- name: VAR1 - value: value1 -image: oci.stackable.tech/sdp/product:latest -imagePullPolicy: Always -name: git-sync-0 -resources: - limits: - cpu: 200m - memory: 64Mi - requests: - cpu: 100m - memory: 64Mi -volumeMounts: -- mountPath: /tmp/git - name: content-from-git-0 -- mountPath: /stackable/log - name: log-volume -- mountPath: /mnt/extra-volume - name: extra-volume -- mountPath: /stackable/gitssh-0 - name: ssh-keys-info-0 -"#, - serde_yaml::to_string(&git_sync_resources.git_sync_containers.first()).unwrap() - ); - - assert_eq!(1, git_sync_resources.git_sync_init_containers.len()); - - assert_eq!( - r#"args: -- |- - mkdir --parents /stackable/log/git-sync-0-init && exec > >(tee /stackable/log/git-sync-0-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-0-init/container.stderr.log >&2) - /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=true --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git -command: -- /bin/bash -- -x -- -euo -- pipefail -- -c -env: -- name: GITSYNC_SSH_KEY_FILE - value: /stackable/gitssh-0/key -- name: GITSYNC_SSH_KNOWN_HOSTS_FILE - value: /stackable/gitssh-0/knownHosts -- name: VAR1 - value: value1 -image: oci.stackable.tech/sdp/product:latest -imagePullPolicy: Always -name: git-sync-0-init -resources: - limits: - cpu: 200m - memory: 64Mi - requests: - cpu: 100m - memory: 64Mi -volumeMounts: -- mountPath: /tmp/git - name: content-from-git-0 -- mountPath: /stackable/log - name: log-volume -- mountPath: /mnt/extra-volume - name: extra-volume -- mountPath: /stackable/gitssh-0 - name: ssh-keys-info-0 -"#, - serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.first()).unwrap() - ); - - assert_eq!(1, git_sync_resources.git_content_volumes.len()); - - assert_eq!( - "emptyDir: {} -name: content-from-git-0 -", - serde_yaml::to_string(&git_sync_resources.git_content_volumes.first()).unwrap() - ); - - assert_eq!(1, git_sync_resources.git_content_volume_mounts.len()); - - assert_eq!( - "mountPath: /stackable/app/git-0 -name: content-from-git-0 -", - serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.first()).unwrap() - ); - - assert_eq!(1, git_sync_resources.git_content_folders.len()); - - assert_eq!( - "/stackable/app/git-0/current/", - git_sync_resources - .git_content_folders_as_string() - .first() - .unwrap() - ); - - assert_eq!(1, git_sync_resources.git_ssh_volumes.len()); - - assert_eq!( - "name: ssh-keys-info-0 -secret: - optional: false - secretName: git-sync-ssh -", - serde_yaml::to_string(&git_sync_resources.git_ssh_volumes.first()).unwrap() - ); - } } diff --git a/crates/stackable-operator/src/crd/git_sync/v1alpha2_impl.rs b/crates/stackable-operator/src/crd/git_sync/v1alpha2_impl.rs new file mode 100644 index 000000000..5ac1b50ed --- /dev/null +++ b/crates/stackable-operator/src/crd/git_sync/v1alpha2_impl.rs @@ -0,0 +1,1130 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use k8s_openapi::api::core::v1::{ + Container, EmptyDirVolumeSource, EnvVar, EnvVarSource, SecretKeySelector, Volume, VolumeMount, +}; +use snafu::{ResultExt, Snafu}; +use stackable_shared::time::Duration; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use crate::{ + builder::pod::{ + container::ContainerBuilder, + resources::ResourceRequirementsBuilder, + volume::{VolumeBuilder, VolumeMountBuilder}, + }, + commons::product_image_selection::ResolvedProductImage, + crd::git_sync::v1alpha2::{Credentials, GitSync}, + product_config_utils::insert_or_update_env_vars, + product_logging::{ + framework::capture_shell_output, + spec::{ContainerLogConfig, ContainerLogConfigChoice}, + }, + utils::COMMON_BASH_TRAP_FUNCTIONS, +}; + +pub const CONTAINER_NAME_PREFIX: &str = "git-sync"; +pub const VOLUME_NAME_PREFIX: &str = "content-from-git"; +pub const MOUNT_PATH_PREFIX: &str = "/stackable/app/git"; +pub const SSH_VOLUME_NAME_PREFIX: &str = "ssh-keys-info"; +pub const SSH_MOUNT_PATH_PREFIX: &str = "/stackable/gitssh"; +pub const GIT_SYNC_SAFE_DIR_OPTION: &str = "safe.directory"; +pub const GIT_SYNC_ROOT_DIR: &str = "/tmp/git"; +pub const GIT_SYNC_LINK: &str = "current"; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("invalid container name"))] + InvalidContainerName { + source: crate::builder::pod::container::Error, + }, + + #[snafu(display("failed to add needed volumeMount"))] + AddVolumeMount { + source: crate::builder::pod::container::Error, + }, +} + +impl GitSync { + pub(crate) fn default_branch() -> String { + "main".to_string() + } + + pub(crate) fn default_git_folder() -> PathBuf { + PathBuf::from("/") + } + + pub(crate) fn default_depth() -> u32 { + 1 + } + + pub(crate) fn default_wait() -> Duration { + Duration::from_secs(20) + } +} + +/// Kubernetes resources generated from `GitSync` specifications which should be added to the Pod. +#[derive(Default)] +pub struct GitSyncResources { + /// GitSync containers with regular synchronizations + pub git_sync_containers: Vec, + + /// GitSync init containers with a one-time synchronizations + pub git_sync_init_containers: Vec, + + /// GitSync volumes containing the synchronized repository + pub git_content_volumes: Vec, + + /// Volume mounts for the GitSync volumes + pub git_content_volume_mounts: Vec, + + /// Absolute paths to the Git contents in the mounted volumes + pub git_content_folders: Vec, + + /// GitSync volumes containing the synchronized repository + pub git_ssh_volumes: Vec, +} + +impl GitSyncResources { + const LOG_VOLUME_MOUNT_PATH: &str = "/stackable/log"; + + /// Returns whether or not GitSync is enabled. + pub fn is_git_sync_enabled(&self) -> bool { + !self.git_sync_containers.is_empty() + } + + /// Returns the Git content folders as strings + pub fn git_content_folders_as_string(&self) -> Vec { + self.git_content_folders + .iter() + .map(|path| path.to_str().expect("The path names of the git_content_folders are created as valid UTF-8 strings, so Path::to_str should not fail.").to_owned()) + .collect() + } + + /// Creates `GitSyncResources` from the given `GitSync` specifications. + pub fn new( + git_syncs: &[GitSync], + resolved_product_image: &ResolvedProductImage, + extra_env_vars: &[EnvVar], + extra_volume_mounts: &[VolumeMount], + log_volume_name: &str, + container_log_config: &ContainerLogConfig, + ) -> Result { + let mut resources = GitSyncResources::default(); + + for (i, git_sync) in git_syncs.iter().enumerate() { + let mut env_vars = vec![]; + + if let Some(Credentials::BasicAuthSecretName(basic_auth_secret_name)) = + &git_sync.credentials + { + env_vars.push(GitSyncResources::env_var_from_secret( + "GITSYNC_USERNAME", + basic_auth_secret_name, + "user", + )); + env_vars.push(GitSyncResources::env_var_from_secret( + "GITSYNC_PASSWORD", + basic_auth_secret_name, + "password", + )); + } + if let Some(Credentials::SshPrivateKeySecretName { .. }) = git_sync.credentials { + env_vars.push(EnvVar { + name: "GITSYNC_SSH_KEY_FILE".to_owned(), + value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/key").to_owned()), + value_from: None, + }); + env_vars.push(EnvVar { + name: "GITSYNC_SSH_KNOWN_HOSTS_FILE".to_owned(), + value: Some(format!("{SSH_MOUNT_PATH_PREFIX}-{i}/knownHosts").to_owned()), + value_from: None, + }); + } + + env_vars = insert_or_update_env_vars(&env_vars, extra_env_vars); + + let volume_name = format!("{VOLUME_NAME_PREFIX}-{i}"); + let mount_path = format!("{MOUNT_PATH_PREFIX}-{i}"); + + let git_sync_root_volume_mount = VolumeMount { + name: volume_name.clone(), + mount_path: GIT_SYNC_ROOT_DIR.to_owned(), + ..VolumeMount::default() + }; + + let log_volume_mount = VolumeMount { + name: log_volume_name.to_string(), + mount_path: Self::LOG_VOLUME_MOUNT_PATH.to_string(), + ..VolumeMount::default() + }; + + let mut git_sync_container_volume_mounts = + vec![git_sync_root_volume_mount, log_volume_mount]; + + git_sync_container_volume_mounts.extend_from_slice(extra_volume_mounts); + + if let Some(Credentials::SshPrivateKeySecretName(_)) = git_sync.credentials { + let ssh_mount_path = format!("{SSH_MOUNT_PATH_PREFIX}-{i}"); + let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); + + let ssh_volume_mount = + VolumeMountBuilder::new(ssh_volume_name, ssh_mount_path).build(); + git_sync_container_volume_mounts.push(ssh_volume_mount); + } + + let container = Self::create_git_sync_container( + &format!("{CONTAINER_NAME_PREFIX}-{i}"), + resolved_product_image, + git_sync, + false, + &env_vars, + &git_sync_container_volume_mounts, + container_log_config, + )?; + + let init_container = Self::create_git_sync_container( + &format!("{CONTAINER_NAME_PREFIX}-{i}-init"), + resolved_product_image, + git_sync, + true, + &env_vars, + &git_sync_container_volume_mounts, + container_log_config, + )?; + + let volume = VolumeBuilder::new(volume_name.clone()) + .empty_dir(EmptyDirVolumeSource::default()) + .build(); + + let git_content_volume_mount = VolumeMount { + name: volume_name.clone(), + mount_path: mount_path.clone(), + ..VolumeMount::default() + }; + + let mut git_content_folder = PathBuf::from(mount_path); + let relative_git_folder = git_sync + .git_folder + .strip_prefix("/") + .unwrap_or(&git_sync.git_folder); + git_content_folder.push(GIT_SYNC_LINK); + git_content_folder.push(relative_git_folder); + + resources.git_sync_containers.push(container); + resources.git_sync_init_containers.push(init_container); + resources.git_content_volumes.push(volume); + resources + .git_content_volume_mounts + .push(git_content_volume_mount); + resources.git_content_folders.push(git_content_folder); + + if let Some(Credentials::SshPrivateKeySecretName(ssh_private_key_secret_name)) = + &git_sync.credentials + { + let ssh_volume_name = format!("{SSH_VOLUME_NAME_PREFIX}-{i}"); + + let ssh_secret_volume = VolumeBuilder::new(&ssh_volume_name) + .with_secret(ssh_private_key_secret_name, false) + .build(); + resources.git_ssh_volumes.push(ssh_secret_volume); + } + } + + Ok(resources) + } + + fn create_git_sync_container( + container_name: &str, + resolved_product_image: &ResolvedProductImage, + git_sync: &GitSync, + one_time: bool, + env_vars: &[EnvVar], + volume_mounts: &[VolumeMount], + container_log_config: &ContainerLogConfig, + ) -> Result { + let container = ContainerBuilder::new(container_name) + .context(InvalidContainerNameSnafu)? + .image_from_product_image(resolved_product_image) + .command(vec![ + "/bin/bash".to_string(), + "-x".to_string(), + "-euo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![Self::create_git_sync_shell_script( + container_name, + git_sync, + one_time, + container_log_config, + )]) + .add_env_vars(env_vars.into()) + .add_volume_mounts(volume_mounts.to_vec()) + .context(AddVolumeMountSnafu)? + .resources( + ResourceRequirementsBuilder::new() + .with_cpu_request("100m") + .with_cpu_limit("200m") + .with_memory_request("64Mi") + .with_memory_limit("64Mi") + .build(), + ) + .build(); + Ok(container) + } + + fn create_git_sync_shell_script( + container_name: &str, + git_sync: &GitSync, + one_time: bool, + container_log_config: &ContainerLogConfig, + ) -> String { + let internal_args = BTreeMap::from([ + ("--repo".to_string(), git_sync.repo.as_str().to_owned()), + ("--ref".to_string(), git_sync.branch.to_owned()), + ("--depth".to_string(), git_sync.depth.to_string()), + ( + "--period".to_string(), + format!("{}s", git_sync.wait.as_secs()), + ), + ("--link".to_string(), GIT_SYNC_LINK.to_string()), + ("--root".to_string(), GIT_SYNC_ROOT_DIR.to_string()), + ("--one-time".to_string(), one_time.to_string()), + ]); + + let internal_git_config = BTreeMap::from([( + GIT_SYNC_SAFE_DIR_OPTION.to_owned(), + GIT_SYNC_ROOT_DIR.to_owned(), + )]); + + let mut git_sync_config = git_sync.git_sync_conf.clone(); + + // The key and value in Git configs are separated by a colon, but both can contain either + // escaped colons or unescaped colons if enclosed in quotes. To avoid parsing, just a String + // is used instead of a key-value pair. + let user_defined_git_config = git_sync_config.remove("--git-config"); + + if let Some(git_config) = &user_defined_git_config { + // Roughly check if the user defined Git config contains an internally defined config + // and emit a warning in case. + internal_git_config + .keys() + .filter(|key| git_config.contains(*key)) + .for_each(|key| { + tracing::warn!( + "The Git config option {git_config:?} contains a value for {key} that \ + overrides the value of this operator. Git-sync functionality will probably \ + not work as expected!" + ); + }); + } + + // The user-defined Git config is just appended. + // The user is responsible for escaping special characters like `:` and `,`. + let git_config = internal_git_config + .into_iter() + .map(|(key, value)| format!("{key}:{value}")) + .chain(user_defined_git_config) + .collect::>() + .join(","); + + let mut user_defined_args = BTreeMap::new(); + + for (key, value) in git_sync_config { + if internal_args.contains_key(&key) { + tracing::warn!( + "The git-sync option {key:?} is already internally defined and will be ignored." + ); + } else { + // The user-defined arguments are not validated. + user_defined_args.insert(key, value); + } + } + + let mut args = internal_args; + args.extend(user_defined_args); + args.insert("--git-config".to_string(), format!("'{git_config}'")); + + let args_string = args + .into_iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join(" "); + + let mut shell_script = String::new(); + + if let ContainerLogConfig { + choice: Some(ContainerLogConfigChoice::Automatic(log_config)), + } = container_log_config + { + shell_script.push_str(&capture_shell_output( + Self::LOG_VOLUME_MOUNT_PATH, + container_name, + log_config, + )); + shell_script.push('\n'); + }; + + let git_sync_command = format!("/stackable/git-sync {args_string}"); + + if one_time { + shell_script.push_str(&git_sync_command); + } else { + // Run the git-sync command in the background + shell_script.push_str(&format!( + "{COMMON_BASH_TRAP_FUNCTIONS} +prepare_signal_handlers +{git_sync_command} & +wait_for_termination $!" + )) + } + + shell_script + } + + fn env_var_from_secret( + var_name: impl Into, + secret: impl Into, + secret_key: impl Into, + ) -> EnvVar { + EnvVar { + name: var_name.into(), + value_from: Some(EnvVarSource { + secret_key_ref: Some(SecretKeySelector { + name: secret.into(), + key: secret_key.into(), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + config::fragment::validate, product_config_utils::env_vars_from, + product_logging::spec::default_container_log_config, utils::yaml_from_str_singleton_map, + }; + + #[test] + fn test_no_git_sync() { + let git_syncs = []; + + let resolved_product_image = ResolvedProductImage { + image: "oci.stackable.tech/sdp/product:latest".to_string(), + app_version_label_value: "1.0.0-latest" + .parse() + .expect("static app version label is always valid"), + product_version: "1.0.0".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + }; + + let extra_env_vars = []; + + let extra_volume_mounts = []; + + let git_sync_resources = GitSyncResources::new( + &git_syncs, + &resolved_product_image, + &extra_env_vars, + &extra_volume_mounts, + "log-volume", + &validate(default_container_log_config()).unwrap(), + ) + .unwrap(); + + assert!(!git_sync_resources.is_git_sync_enabled()); + assert!(git_sync_resources.git_sync_containers.is_empty()); + assert!(git_sync_resources.git_sync_init_containers.is_empty()); + assert!(git_sync_resources.git_content_volumes.is_empty()); + assert!(git_sync_resources.git_content_volume_mounts.is_empty()); + assert!(git_sync_resources.git_content_folders.is_empty()); + } + + #[test] + fn test_multiple_git_syncs() { + let git_sync_spec = r#" + # GitSync with defaults + - repo: https://github.com/stackabletech/repo1 + + # GitSync with usual configuration + - repo: https://github.com/stackabletech/repo2 + branch: trunk + gitFolder: "" + depth: 3 + wait: 1m + credentials: + basicAuthSecretName: git-credentials + gitSyncConf: + --rev: HEAD + --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt + + # GitSync with unusual configuration + - repo: https://github.com/stackabletech/repo3 + branch: feat/git-sync + # leading slashes should be removed + gitFolder: ////folder + gitSyncConf: + --depth: internal option which should be ignored + --link: internal option which should be ignored + --period: internal option which should be ignored + --ref: internal option which should be ignored + --repo: internal option which should be ignored + --root: internal option which should be ignored + # safe.directory should be accepted but a warning will be emitted + --git-config: key:value,safe.directory:/safe-dir + "#; + + let git_syncs: Vec = yaml_from_str_singleton_map(git_sync_spec).unwrap(); + + let resolved_product_image = ResolvedProductImage { + image: "oci.stackable.tech/sdp/product:latest".to_string(), + app_version_label_value: "1.0.0-latest" + .parse() + .expect("static app version label is always valid"), + product_version: "1.0.0".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + }; + + let extra_env_vars = env_vars_from([ + ("VAR1", "value1"), + ("GITSYNC_USERNAME", "overriden-username"), + ]); + + let extra_volume_mounts = [VolumeMount { + name: "extra-volume".to_string(), + mount_path: "/mnt/extra-volume".to_string(), + ..VolumeMount::default() + }]; + + let git_sync_resources = GitSyncResources::new( + &git_syncs, + &resolved_product_image, + &extra_env_vars, + &extra_volume_mounts, + "log-volume", + &validate(default_container_log_config()).unwrap(), + ) + .unwrap(); + + assert!(git_sync_resources.is_git_sync_enabled()); + + assert_eq!(3, git_sync_resources.git_sync_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0 && exec > >(tee /stackable/log/git-sync-0/container.stdout.log) 2> >(tee /stackable/log/git-sync-0/container.stderr.log >&2) + + prepare_signal_handlers() + { + unset term_child_pid + unset term_kill_needed + trap 'handle_term_signal' TERM + } + + handle_term_signal() + { + if [ "${term_child_pid}" ]; then + kill -TERM "${term_child_pid}" 2>/dev/null + else + term_kill_needed="yes" + fi + } + + wait_for_termination() + { + set +e + term_child_pid=$1 + if [[ -v term_kill_needed ]]; then + kill -TERM "${term_child_pid}" 2>/dev/null + fi + wait ${term_child_pid} 2>/dev/null + trap - TERM + wait ${term_child_pid} 2>/dev/null + set -e + } + + prepare_signal_handlers + /stackable/git-sync --depth=1 --git-config='safe.directory:/tmp/git' --link=current --one-time=false --period=20s --ref=main --repo=https://github.com/stackabletech/repo1 --root=/tmp/git & + wait_for_termination $! +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0 +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_containers.first()).unwrap() + ); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-1 && exec > >(tee /stackable/log/git-sync-1/container.stdout.log) 2> >(tee /stackable/log/git-sync-1/container.stderr.log >&2) + + prepare_signal_handlers() + { + unset term_child_pid + unset term_kill_needed + trap 'handle_term_signal' TERM + } + + handle_term_signal() + { + if [ "${term_child_pid}" ]; then + kill -TERM "${term_child_pid}" 2>/dev/null + else + term_kill_needed="yes" + fi + } + + wait_for_termination() + { + set +e + term_child_pid=$1 + if [[ -v term_kill_needed ]]; then + kill -TERM "${term_child_pid}" 2>/dev/null + fi + wait ${term_child_pid} 2>/dev/null + trap - TERM + wait ${term_child_pid} 2>/dev/null + set -e + } + + prepare_signal_handlers + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=false --period=60s --ref=trunk --repo=https://github.com/stackabletech/repo2 --rev=HEAD --root=/tmp/git & + wait_for_termination $! +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: git-credentials +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-1 +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-1 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_containers.get(1)).unwrap() + ); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-2 && exec > >(tee /stackable/log/git-sync-2/container.stdout.log) 2> >(tee /stackable/log/git-sync-2/container.stderr.log >&2) + + prepare_signal_handlers() + { + unset term_child_pid + unset term_kill_needed + trap 'handle_term_signal' TERM + } + + handle_term_signal() + { + if [ "${term_child_pid}" ]; then + kill -TERM "${term_child_pid}" 2>/dev/null + else + term_kill_needed="yes" + fi + } + + wait_for_termination() + { + set +e + term_child_pid=$1 + if [[ -v term_kill_needed ]]; then + kill -TERM "${term_child_pid}" 2>/dev/null + fi + wait ${term_child_pid} 2>/dev/null + trap - TERM + wait ${term_child_pid} 2>/dev/null + set -e + } + + prepare_signal_handlers + /stackable/git-sync --depth=1 --git-config='safe.directory:/tmp/git,key:value,safe.directory:/safe-dir' --link=current --one-time=false --period=20s --ref=feat/git-sync --repo=https://github.com/stackabletech/repo3 --root=/tmp/git & + wait_for_termination $! +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-2 +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-2 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_containers.get(2)).unwrap() + ); + + assert_eq!(3, git_sync_resources.git_sync_init_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0-init && exec > >(tee /stackable/log/git-sync-0-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-0-init/container.stderr.log >&2) + /stackable/git-sync --depth=1 --git-config='safe.directory:/tmp/git' --link=current --one-time=true --period=20s --ref=main --repo=https://github.com/stackabletech/repo1 --root=/tmp/git +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0-init +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.first()).unwrap() + ); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-1-init && exec > >(tee /stackable/log/git-sync-1-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-1-init/container.stderr.log >&2) + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=true --period=60s --ref=trunk --repo=https://github.com/stackabletech/repo2 --rev=HEAD --root=/tmp/git +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: git-credentials +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-1-init +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-1 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.get(1)).unwrap() + ); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-2-init && exec > >(tee /stackable/log/git-sync-2-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-2-init/container.stderr.log >&2) + /stackable/git-sync --depth=1 --git-config='safe.directory:/tmp/git,key:value,safe.directory:/safe-dir' --link=current --one-time=true --period=20s --ref=feat/git-sync --repo=https://github.com/stackabletech/repo3 --root=/tmp/git +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_USERNAME + value: overriden-username +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-2-init +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-2 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.get(2)).unwrap() + ); + + assert_eq!(3, git_sync_resources.git_content_volumes.len()); + + assert_eq!( + "emptyDir: {} +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volumes.first()).unwrap() + ); + + assert_eq!( + "emptyDir: {} +name: content-from-git-1 +", + serde_yaml::to_string(&git_sync_resources.git_content_volumes.get(1)).unwrap() + ); + + assert_eq!( + "emptyDir: {} +name: content-from-git-2 +", + serde_yaml::to_string(&git_sync_resources.git_content_volumes.get(2)).unwrap() + ); + + assert_eq!(3, git_sync_resources.git_content_volume_mounts.len()); + + assert_eq!( + "mountPath: /stackable/app/git-0 +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.first()).unwrap() + ); + + assert_eq!( + "mountPath: /stackable/app/git-1 +name: content-from-git-1 +", + serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.get(1)).unwrap() + ); + + assert_eq!( + "mountPath: /stackable/app/git-2 +name: content-from-git-2 +", + serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.get(2)).unwrap() + ); + + assert_eq!(3, git_sync_resources.git_content_folders.len()); + + assert_eq!( + "/stackable/app/git-0/current/", + git_sync_resources + .git_content_folders_as_string() + .first() + .unwrap() + ); + + assert_eq!( + "/stackable/app/git-1/current/", + git_sync_resources + .git_content_folders_as_string() + .get(1) + .unwrap() + ); + + assert_eq!( + "/stackable/app/git-2/current/folder", + git_sync_resources + .git_content_folders_as_string() + .get(2) + .unwrap() + ); + } + + #[test] + fn test_git_sync_ssh() { + let git_sync_spec = r#" + # GitSync using SSH + - repo: ssh://git@github.com/stackabletech/repo.git + branch: trunk + gitFolder: "" + depth: 3 + wait: 1m + credentials: + sshPrivateKeySecretName: git-sync-ssh + gitSyncConf: + --rev: HEAD + --git-config: http.sslCAInfo:/tmp/ca-cert/ca.crt + "#; + + let git_syncs: Vec = yaml_from_str_singleton_map(git_sync_spec).unwrap(); + + let resolved_product_image = ResolvedProductImage { + image: "oci.stackable.tech/sdp/product:latest".to_string(), + app_version_label_value: "1.0.0-latest" + .parse() + .expect("static app version label is always valid"), + product_version: "1.0.0".to_string(), + image_pull_policy: "Always".to_string(), + pull_secrets: None, + }; + + let extra_env_vars = env_vars_from([("VAR1", "value1")]); + + let extra_volume_mounts = [VolumeMount { + name: "extra-volume".to_string(), + mount_path: "/mnt/extra-volume".to_string(), + ..VolumeMount::default() + }]; + + let git_sync_resources = GitSyncResources::new( + &git_syncs, + &resolved_product_image, + &extra_env_vars, + &extra_volume_mounts, + "log-volume", + &validate(default_container_log_config()).unwrap(), + ) + .unwrap(); + + assert!(git_sync_resources.is_git_sync_enabled()); + + assert_eq!(1, git_sync_resources.git_sync_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0 && exec > >(tee /stackable/log/git-sync-0/container.stdout.log) 2> >(tee /stackable/log/git-sync-0/container.stderr.log >&2) + + prepare_signal_handlers() + { + unset term_child_pid + unset term_kill_needed + trap 'handle_term_signal' TERM + } + + handle_term_signal() + { + if [ "${term_child_pid}" ]; then + kill -TERM "${term_child_pid}" 2>/dev/null + else + term_kill_needed="yes" + fi + } + + wait_for_termination() + { + set +e + term_child_pid=$1 + if [[ -v term_kill_needed ]]; then + kill -TERM "${term_child_pid}" 2>/dev/null + fi + wait ${term_child_pid} 2>/dev/null + trap - TERM + wait ${term_child_pid} 2>/dev/null + set -e + } + + prepare_signal_handlers + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=false --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git & + wait_for_termination $! +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_SSH_KEY_FILE + value: /stackable/gitssh-0/key +- name: GITSYNC_SSH_KNOWN_HOSTS_FILE + value: /stackable/gitssh-0/knownHosts +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0 +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +- mountPath: /stackable/gitssh-0 + name: ssh-keys-info-0 +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_containers.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_sync_init_containers.len()); + + assert_eq!( + r#"args: +- |- + mkdir --parents /stackable/log/git-sync-0-init && exec > >(tee /stackable/log/git-sync-0-init/container.stdout.log) 2> >(tee /stackable/log/git-sync-0-init/container.stderr.log >&2) + /stackable/git-sync --depth=3 --git-config='safe.directory:/tmp/git,http.sslCAInfo:/tmp/ca-cert/ca.crt' --link=current --one-time=true --period=60s --ref=trunk --repo=ssh://git@github.com/stackabletech/repo.git --rev=HEAD --root=/tmp/git +command: +- /bin/bash +- -x +- -euo +- pipefail +- -c +env: +- name: GITSYNC_SSH_KEY_FILE + value: /stackable/gitssh-0/key +- name: GITSYNC_SSH_KNOWN_HOSTS_FILE + value: /stackable/gitssh-0/knownHosts +- name: VAR1 + value: value1 +image: oci.stackable.tech/sdp/product:latest +imagePullPolicy: Always +name: git-sync-0-init +resources: + limits: + cpu: 200m + memory: 64Mi + requests: + cpu: 100m + memory: 64Mi +volumeMounts: +- mountPath: /tmp/git + name: content-from-git-0 +- mountPath: /stackable/log + name: log-volume +- mountPath: /mnt/extra-volume + name: extra-volume +- mountPath: /stackable/gitssh-0 + name: ssh-keys-info-0 +"#, + serde_yaml::to_string(&git_sync_resources.git_sync_init_containers.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_volumes.len()); + + assert_eq!( + "emptyDir: {} +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volumes.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_volume_mounts.len()); + + assert_eq!( + "mountPath: /stackable/app/git-0 +name: content-from-git-0 +", + serde_yaml::to_string(&git_sync_resources.git_content_volume_mounts.first()).unwrap() + ); + + assert_eq!(1, git_sync_resources.git_content_folders.len()); + + assert_eq!( + "/stackable/app/git-0/current/", + git_sync_resources + .git_content_folders_as_string() + .first() + .unwrap() + ); + + assert_eq!(1, git_sync_resources.git_ssh_volumes.len()); + + assert_eq!( + "name: ssh-keys-info-0 +secret: + optional: false + secretName: git-sync-ssh +", + serde_yaml::to_string(&git_sync_resources.git_ssh_volumes.first()).unwrap() + ); + } +} diff --git a/crates/xtask/src/crd/dummy.rs b/crates/xtask/src/crd/dummy.rs index 28a0c3f91..e886e1321 100644 --- a/crates/xtask/src/crd/dummy.rs +++ b/crates/xtask/src/crd/dummy.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use stackable_operator::{ commons::resources::{JvmHeapLimits, Resources}, config::fragment::Fragment, - crd::git_sync::v1alpha1::GitSync, + crd::git_sync::v1alpha2::GitSync, deep_merger::ObjectOverrides, kube::CustomResource, role_utils::Role,