Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4b892dd
add ssh key information
adwk67 Nov 27, 2025
a18aeb4
remove ssh_known_hosts flag and git_ssh_volume_mounts
adwk67 Dec 1, 2025
ff025d4
Merge branch 'main' into feat/gitsync-ssh
adwk67 Dec 1, 2025
bed929e
pre-commit fix
adwk67 Dec 1, 2025
843820c
added check for multiple credential types
adwk67 Dec 1, 2025
3ab7a5f
changelog
adwk67 Dec 1, 2025
3bcde90
add gitsync to dummy cluster
adwk67 Dec 4, 2025
0e024d4
replace fields with enum
adwk67 Dec 8, 2025
f31b965
flatten enum and re-name fields
adwk67 Dec 18, 2025
c537512
fix conflict
adwk67 Dec 18, 2025
6c26212
Update crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs
adwk67 Dec 18, 2025
41ba13c
Update crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs
adwk67 Dec 18, 2025
d14cc6a
make enum untagged
adwk67 Dec 18, 2025
fbcb301
make enum untagged: corrected
adwk67 Dec 18, 2025
ec980f4
changelog: mark change as breaking
adwk67 Dec 18, 2025
8b94f6a
wip: versioning
adwk67 Dec 18, 2025
92d6dc4
added comment
adwk67 Dec 23, 2025
dbe2250
Merge branch 'main' into feat/gitsync-ssh
adwk67 Dec 23, 2025
1ebb612
fix/webhook): Don't crash on dropped initial_reconcile_tx channel
sbernauer Jan 7, 2026
dea589a
fix/webhook): Don't crash on dropped initial_reconcile_tx channel
sbernauer Jan 7, 2026
ff80cd5
changelog
sbernauer Jan 7, 2026
bfdffa8
changelog
sbernauer Jan 7, 2026
af06b5a
merge conflict
adwk67 Jan 7, 2026
69cac16
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 7, 2026
8368484
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 7, 2026
522bfc5
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 7, 2026
08cb207
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 12, 2026
8e306ef
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 12, 2026
2015fb6
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 14, 2026
91c98d3
corrected changelog
adwk67 Jan 14, 2026
354c9e6
move default functions to non-versioned functions
adwk67 Jan 15, 2026
2fec9f1
Merge branch 'main' into feat/gitsync-ssh
adwk67 Jan 15, 2026
d635dd0
corrected order in changelog
adwk67 Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 10 additions & 1 deletion crates/stackable-operator/src/crd/git_sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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<String>,
Expand All @@ -63,5 +64,13 @@ pub mod versioned {
/// [example]: DOCS_BASE_URL_PLACEHOLDER/airflow/usage-guide/mounting-dags#_example
#[serde(default)]
pub git_sync_conf: BTreeMap<String, String>,

/// 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<String>,
}
}
254 changes: 253 additions & 1 deletion crates/stackable-operator/src/crd/git_sync/v1alpha1_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand All @@ -40,6 +44,9 @@ pub enum Error {
AddVolumeMount {
source: crate::builder::pod::container::Error,
},

#[snafu(display("failed to declare unique credentials"))]
MultipleCredentials,
}

impl GitSync {
Expand Down Expand Up @@ -77,6 +84,9 @@ pub struct GitSyncResources {

/// Absolute paths to the Git contents in the mounted volumes
pub git_content_folders: Vec<PathBuf>,

/// GitSync volumes containing the synchronized repository
pub git_ssh_volumes: Vec<Volume>,
}

impl GitSyncResources {
Expand Down Expand Up @@ -107,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(
Expand All @@ -120,6 +135,19 @@ 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,
});
}

env_vars = insert_or_update_env_vars(&env_vars, extra_env_vars);

let volume_name = format!("{VOLUME_NAME_PREFIX}-{i}");
Expand All @@ -139,8 +167,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,
Expand Down Expand Up @@ -186,6 +224,15 @@ 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_secret_volume = VolumeBuilder::new(&ssh_volume_name)
.with_secret(get_ssh_secret, false)
.build();
resources.git_ssh_volumes.push(ssh_secret_volume);
}
}

Ok(resources)
Expand Down Expand Up @@ -876,4 +923,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<GitSync> = 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()
);
}
}