Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Git sync: add support for CAs ([#1154]).

[#1154]: https://github.com/stackabletech/operator-rs/pull/1154

## [0.106.2] - 2026-02-26

### Changed
Expand Down
50 changes: 50 additions & 0 deletions crates/stackable-operator/crds/DummyCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,56 @@ 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
tls:
default:
verification:
server:
caCert:
webPki: {}
description: Configure a TLS connection. If not specified it will default to webPki validation.
nullable: true
properties:
verification:
description: The verification method used to verify the certificates of the server and/or the client.
oneOf:
- required:
- none
- required:
- server
properties:
none:
description: Use TLS but don't verify certificates.
type: object
server:
description: Use TLS and a CA certificate to verify the server.
properties:
caCert:
description: CA cert to verify the server.
oneOf:
- required:
- webPki
- required:
- secretClass
properties:
secretClass:
description: |-
Name of the [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass) which will provide the CA certificate.
Note that a SecretClass does not need to have a key but can also work with just a CA certificate,
so if you got provided with a CA cert but don't have access to the key you can still use this method.
type: string
webPki:
description: |-
Use TLS and the CA certificates trusted by the common web browsers to verify the server.
This can be useful when you e.g. use public AWS S3 or other public available services.
type: object
type: object
required:
- caCert
type: object
type: object
required:
- verification
type: object
wait:
default: 20s
description: |-
Expand Down
83 changes: 83 additions & 0 deletions crates/stackable-operator/src/commons/tls_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub enum TlsClientDetailsError {
},
}

#[repr(transparent)]
#[derive(
Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
Expand All @@ -35,6 +36,40 @@ pub struct TlsClientDetails {
pub tls: Option<Tls>,
}

#[repr(transparent)]
#[derive(
Clone, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "camelCase")]
pub struct TlsClientDetailsWithSecureDefaults {
/// Configure a TLS connection. If not specified it will default to webPki validation.
#[serde(default = "default_web_pki_tls")]
pub tls: Option<Tls>,
}

impl std::ops::Deref for TlsClientDetailsWithSecureDefaults {
type Target = TlsClientDetails;

fn deref(&self) -> &TlsClientDetails {
// SAFETY: both types are `#[repr(transparent)]` over `Option<Tls>`, so they share
// the same memory layout and this cast is sound.
//
// This cannot silently break due to struct changes: `#[repr(transparent)]` requires
// exactly one non-zero-sized field, so adding a second real field to either struct
// is a compile error. The only scenario that would NOT be caught at compile time is
// deliberately removing `#[repr(transparent)]` from one of the two structs.
unsafe { &*(self as *const Self as *const TlsClientDetails) }
}
}

fn default_web_pki_tls() -> Option<Tls> {
Some(Tls {
verification: TlsVerification::Server(TlsServerVerification {
ca_cert: CaCert::WebPki {},
}),
})
}

impl TlsClientDetails {
/// This functions adds
///
Expand Down Expand Up @@ -165,3 +200,51 @@ pub enum CaCert {
/// so if you got provided with a CA cert but don't have access to the key you can still use this method.
SecretClass(String),
}

#[cfg(test)]
mod tests {
use super::*;
use crate::utils::yaml_from_str_singleton_map;

#[test]
fn tls_client_details_with_secure_defaults_deserialization() {
// No tls key at all → WebPki default kicks in
let parsed: TlsClientDetailsWithSecureDefaults =
yaml_from_str_singleton_map("{}").expect("failed to deserialize empty input");
assert_eq!(parsed.tls, default_web_pki_tls());

// Explicit null → opt out of TLS entirely
let parsed: TlsClientDetailsWithSecureDefaults =
yaml_from_str_singleton_map("tls: null").expect("failed to deserialize tls: null");
assert_eq!(parsed.tls, None);

// Explicit SecretClass value is preserved as-is
let parsed: TlsClientDetailsWithSecureDefaults = yaml_from_str_singleton_map(
"tls:
verification:
server:
caCert:
secretClass: my-ca",
)
.expect("failed to deserialize secretClass");
assert_eq!(
parsed.tls,
Some(Tls {
verification: TlsVerification::Server(TlsServerVerification {
ca_cert: CaCert::SecretClass("my-ca".to_owned()),
}),
})
);
}

#[test]
#[allow(clippy::explicit_auto_deref)]
fn tls_client_details_with_secure_defaults_deref() {
let secure: TlsClientDetailsWithSecureDefaults =
yaml_from_str_singleton_map("{}").expect("failed to deserialize");

// Deref must not panic and must expose the same tls value
let tls_client_details: &TlsClientDetails = &*secure;
assert_eq!(tls_client_details.tls, secure.tls);
}
}
12 changes: 11 additions & 1 deletion crates/stackable-operator/src/crd/git_sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ use serde::{Deserialize, Serialize};
use stackable_shared::time::Duration;
use url::Url;

use crate::{crd::git_sync::v1alpha2::Credentials, versioned::versioned};
use crate::{
commons::tls_verification::TlsClientDetailsWithSecureDefaults,
crd::git_sync::v1alpha2::Credentials, versioned::versioned,
};

mod v1alpha1_impl;
mod v1alpha2_impl;

#[versioned(version(name = "v1alpha1"), version(name = "v1alpha2"))]
pub mod versioned {

pub mod v1alpha1 {
pub use v1alpha1_impl::{Error, GitSyncResources};
}
Expand Down Expand Up @@ -68,6 +72,12 @@ pub mod versioned {
downgrade_with = credentials_to_secret
))]
pub credentials: Option<Credentials>,

/// An optional field used for referencing CA certificates that will be used to verify the git server's TLS certificate by passing it to the git config option `http.sslCAInfo` passed with the gitsync command. The secret must have a key named `ca.crt` whose value is the PEM-encoded certificate bundle.
/// If `http.sslCAInfo` is also set via `gitSyncConf` (the `--git-config` option) then a warning will be logged.
/// If not specified no TLS will be used, defaulting to github/lab using commonly-recognised certificates.
#[serde(flatten)]
pub tls: TlsClientDetailsWithSecureDefaults,
}

#[derive(strum::Display, Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
Expand Down
Loading