Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 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

- Support exporting the TrustStore CA certificate information to Secrets or ConfigMaps ([#597]).

[#597]: https://github.com/stackabletech/secret-operator/pull/597

## [25.7.0] - 2025-07-23

## [25.7.0-rc1] - 2025-07-18
Expand Down
12 changes: 12 additions & 0 deletions deploy/helm/secret-operator/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,18 @@ spec:
secretClassName:
description: The name of the SecretClass that the request concerns.
type: string
targetKind:
default: ConfigMap
description: |-
Which Kubernetes kind should be used to output the requested information to.

The trust information (such as a `ca.crt`) can be considered public information, so we put it in a `ConfigMap` by default. However, some tools might require it to be placed in a `Secret`, so we also support that.

Can be either `ConfigMap` or `Secret`, defaults to `ConfigMap`.
enum:
- Secret
- ConfigMap
type: string
required:
- secretClassName
type: object
Expand Down
19 changes: 19 additions & 0 deletions rust/operator-binary/src/crd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,29 @@ pub struct TrustStoreSpec {
/// The name of the SecretClass that the request concerns.
pub secret_class_name: String,

/// Which Kubernetes kind should be used to output the requested information to.
///
/// The trust information (such as a `ca.crt`) can be considered public information, so we put
/// it in a `ConfigMap` by default. However, some tools might require it to be placed in a
/// `Secret`, so we also support that.
///
/// Can be either `ConfigMap` or `Secret`, defaults to `ConfigMap`.
#[serde(default)]
pub target_kind: TrustStoreOutputType,

/// The [format](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#format) that the data should be converted into.
pub format: Option<SecretFormat>,
}

#[derive(Clone, Debug, Default, PartialEq, JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TrustStoreOutputType {
Secret,

#[default]
ConfigMap,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
76 changes: 56 additions & 20 deletions rust/operator-binary/src/truststore_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use strum::{EnumDiscriminants, IntoStaticStr};
use crate::{
OPERATOR_NAME,
backend::{self, SecretBackendError, TrustSelector},
crd::{SearchNamespaceMatchCondition, SecretClass, TrustStore},
crd::{SearchNamespaceMatchCondition, SecretClass, TrustStore, TrustStoreOutputType},
format::{
self,
well_known::{CompatibilityOptions, NamingOptions},
Expand Down Expand Up @@ -85,6 +85,11 @@ pub async fn start(client: &stackable_operator::client::Client, watch_namespace:
watch_namespace.get_api::<PartialObjectMeta<ConfigMap>>(client),
watcher::Config::default(),
)
// TODO: merge this into the other Secret watch
.owns(
watch_namespace.get_api::<PartialObjectMeta<Secret>>(client),
watcher::Config::default(),
)
.watches(
watch_namespace.get_api::<PartialObjectMeta<ConfigMap>>(client),
watcher::Config::default(),
Expand Down Expand Up @@ -208,7 +213,14 @@ pub enum Error {
source: stackable_operator::client::Error,
config_map: ObjectRef<ConfigMap>,
},

#[snafu(display("failed to apply target {secret} for the TrustStore"))]
ApplyTrustStoreSecret {
source: stackable_operator::client::Error,
secret: ObjectRef<Secret>,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;
impl ReconcilerError for Error {
fn category(&self) -> &'static str {
Expand All @@ -225,6 +237,7 @@ impl ReconcilerError for Error {
Error::FormatData { secret_class, .. } => Some(secret_class.clone().erase()),
Error::BuildOwnerReference { .. } => None,
Error::ApplyTrustStoreConfigMap { config_map, .. } => Some(config_map.clone().erase()),
Error::ApplyTrustStoreSecret { secret, .. } => Some(secret.clone().erase()),
}
}
}
Expand Down Expand Up @@ -267,7 +280,7 @@ async fn reconcile(
.get_trust_data(&selector)
.await
.context(BackendGetTrustDataSnafu)?;
let (Flattened(string_data), Flattened(binary_data)) = trust_data
let trust_file_contents = trust_data
.data
.into_files(
truststore.spec.format,
Expand All @@ -276,30 +289,53 @@ async fn reconcile(
)
.context(FormatDataSnafu {
secret_class: secret_class_ref,
})?
})?;
let (Flattened(string_data), Flattened(binary_data)) = trust_file_contents
.into_iter()
// Try to put valid UTF-8 data into `data`, but fall back to `binary_data` otherwise
// Try to put valid UTF-8 data into `string_data`, but fall back to `binary_data` otherwise
.map(|(k, v)| match String::from_utf8(v) {
Ok(v) => (Some((k, v)), None),
Err(v) => (None, Some((k, ByteString(v.into_bytes())))),
})
.collect();
let trust_cm = ConfigMap {
metadata: ObjectMetaBuilder::new()
.name_and_namespace(truststore)
.ownerreference_from_resource(truststore, None, Some(true))
.context(BuildOwnerReferenceSnafu)?
.build(),
data: Some(string_data),
binary_data: Some(binary_data),
..Default::default()
};
ctx.client
.apply_patch(CONTROLLER_NAME, &trust_cm, &trust_cm)
.await
.context(ApplyTrustStoreConfigMapSnafu {
config_map: &trust_cm,
})?;

let trust_metadata = ObjectMetaBuilder::new()
.name_and_namespace(truststore)
.ownerreference_from_resource(truststore, None, Some(true))
.context(BuildOwnerReferenceSnafu)?
.build();

match truststore.spec.target_kind {
TrustStoreOutputType::ConfigMap => {
let trust_cm = ConfigMap {
metadata: trust_metadata,
data: Some(string_data),
binary_data: Some(binary_data),
..Default::default()
};
ctx.client
.apply_patch(CONTROLLER_NAME, &trust_cm, &trust_cm)
.await
.context(ApplyTrustStoreConfigMapSnafu {
config_map: &trust_cm,
})?;
}
TrustStoreOutputType::Secret => {
let trust_secret = Secret {
metadata: trust_metadata,
string_data: Some(string_data),
data: Some(binary_data),
..Default::default()
};
ctx.client
.apply_patch(CONTROLLER_NAME, &trust_secret, &trust_secret)
.await
.context(ApplyTrustStoreSecretSnafu {
secret: &trust_secret,
})?;
}
}

Ok(controller::Action::await_change())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 02_secretclass.yaml | kubectl apply -f -
5 changes: 0 additions & 5 deletions tests/templates/kuttl/cert-manager-tls/02-secretclass.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions tests/templates/kuttl/cert-manager-tls/10-consumer.yaml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 10_consumer.yaml | kubectl apply -n $NAMESPACE -f -
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < secretclass.yaml | kubectl apply -f -
- script: envsubst '$NAMESPACE' < 01_secretclass.yaml | kubectl apply -f -
---
apiVersion: v1
kind: Secret
Expand Down
2 changes: 1 addition & 1 deletion tests/templates/kuttl/kerberos-ad/02-kinit-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < kinit-client.yaml | kubectl apply -n $NAMESPACE -f -
- script: envsubst '$NAMESPACE' < 02_kinit-client.yaml | kubectl apply -n $NAMESPACE -f -
---
apiVersion: v1
kind: Service
Expand Down
4 changes: 2 additions & 2 deletions tests/templates/kuttl/kerberos/01-install-kdc.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < secretclass.yaml | kubectl apply -f -
- script: envsubst '$NAMESPACE' < listenerclass.yaml | kubectl apply -f -
- script: envsubst '$NAMESPACE' < 01_secretclass.yaml | kubectl apply -f -
- script: envsubst '$NAMESPACE' < 01_listenerclass.yaml | kubectl apply -f -
---
apiVersion: apps/v1
kind: StatefulSet
Expand Down
2 changes: 1 addition & 1 deletion tests/templates/kuttl/kerberos/02-kinit-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < kinit-client.yaml | kubectl apply -n $NAMESPACE -f -
- script: envsubst '$NAMESPACE' < 02_kinit-client.yaml | kubectl apply -n $NAMESPACE -f -
---
apiVersion: v1
kind: Service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 01_secretclass.yaml | kubectl --namespace=$NAMESPACE apply -f -
5 changes: 0 additions & 5 deletions tests/templates/kuttl/tls-truststore/01-secretclass.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ kind: TestAssert
timeout: 5
---
apiVersion: v1
kind: ConfigMap
kind: {{ test_scenario['values']['truststore-target-kind'] }}
metadata:
name: truststore-pem
# data is validated in 03-assert.yaml
---
apiVersion: v1
kind: ConfigMap
kind: {{ test_scenario['values']['truststore-target-kind'] }}
metadata:
name: truststore-pkcs12
# data is validated in 03-assert.yaml
---
{% if test_scenario['values']['truststore-target-kind'] == 'ConfigMap' %}
apiVersion: v1
kind: ConfigMap
metadata:
Expand All @@ -26,3 +27,13 @@ data:
binaryData:
# Should stay binary since it is not legal UTF-8
actuallyBinary: aWxsZWdhbIB1dGYtOA==
{% else %}
apiVersion: v1
kind: Secret
metadata:
name: truststore-k8ssearch
data:
foo: YmFy
baz: aGVsbG8=
actuallyBinary: aWxsZWdhbIB1dGYtOA==
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 02_truststore.yaml | kubectl --namespace=$NAMESPACE apply -f -
5 changes: 0 additions & 5 deletions tests/templates/kuttl/tls-truststore/02-truststore.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ metadata:
spec:
secretClassName: tls-$NAMESPACE
format: tls-pem
targetKind: {{ test_scenario['values']['truststore-target-kind'] }}
---
apiVersion: secrets.stackable.tech/v1alpha1
kind: TrustStore
Expand All @@ -15,10 +16,12 @@ metadata:
spec:
secretClassName: tls-$NAMESPACE
format: tls-pkcs12
targetKind: {{ test_scenario['values']['truststore-target-kind'] }}
---
apiVersion: secrets.stackable.tech/v1alpha1
kind: TrustStore
metadata:
name: truststore-k8ssearch
spec:
secretClassName: k8ssearch-$NAMESPACE
targetKind: {{ test_scenario['values']['truststore-target-kind'] }}
8 changes: 0 additions & 8 deletions tests/templates/kuttl/tls-truststore/03-assert.yaml

This file was deleted.

13 changes: 13 additions & 0 deletions tests/templates/kuttl/tls-truststore/03-assert.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Validate certificates generated by step 02
---
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
timeout: 10
commands:
{% if test_scenario['values']['truststore-target-kind'] == 'ConfigMap' %}
- script: kubectl --namespace=$NAMESPACE get configmap/truststore-pem --output=jsonpath='{.data.ca\.crt}' | openssl x509 -noout
- script: kubectl --namespace=$NAMESPACE get configmap/truststore-pkcs12 --output=jsonpath='{.binaryData.truststore\.p12}' | base64 --decode | openssl pkcs12 -noout -passin 'pass:' -legacy
{% else %}
- script: kubectl --namespace=$NAMESPACE get secret/truststore-pem --output=jsonpath='{.data.ca\.crt}' | base64 --decode | openssl x509 -noout
- script: kubectl --namespace=$NAMESPACE get secret/truststore-pkcs12 --output=jsonpath='{.data.truststore\.p12}' | base64 --decode | openssl pkcs12 -noout -passin 'pass:' -legacy
{% endif %}
5 changes: 5 additions & 0 deletions tests/templates/kuttl/tls/01-create-secretclass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 01_secretclass.yaml | kubectl apply -f -
5 changes: 0 additions & 5 deletions tests/templates/kuttl/tls/01-secretclass.yaml

This file was deleted.

5 changes: 0 additions & 5 deletions tests/templates/kuttl/tls/10-consumer.yaml

This file was deleted.

5 changes: 5 additions & 0 deletions tests/templates/kuttl/tls/10-create-consumer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- script: envsubst '$NAMESPACE' < 10_consumer.yaml | kubectl apply -n $NAMESPACE -f -
5 changes: 5 additions & 0 deletions tests/test-definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@
values:
- false
- true
- name: truststore-target-kind
values:
- ConfigMap
- Secret
tests:
- name: kerberos
dimensions:
- krb5
- openshift
# Requires manual connection to an AD cluster

Check warning on line 31 in tests/test-definition.yaml

View workflow job for this annotation

GitHub Actions / pre-commit

31:5 [comments-indentation] comment not indented like content
# - name: kerberos-ad
# dimensions:
# - krb5
Expand All @@ -40,6 +44,7 @@
- name: tls-truststore
dimensions:
- openshift
- truststore-target-kind
- name: cert-manager-tls
dimensions:
- openshift
Expand Down
Loading