Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bc0e239
feat: Implement annotation restarter.stackable.tech/ignore
siegfriedweber Mar 23, 2026
dc5a83e
test(restarter): Test annotation restarter.stackable.tech/ignore
siegfriedweber Apr 1, 2026
87a00e0
chore: Update changelog
siegfriedweber Apr 1, 2026
42e15d4
Merge branch 'main' into feat/ignore-restarter
siegfriedweber Apr 1, 2026
4ec9e81
docs: Document the annotation "restarter.stackable.tech/ignore"
siegfriedweber Apr 1, 2026
7d3005f
chore: Upgrade to Rust edition 2024
siegfriedweber Apr 2, 2026
faf1a73
feat: Support ignore annotations on StatefulSets
siegfriedweber Apr 2, 2026
26e336b
docs: Document the annotations "restarter.stackable.tech/ignore-confi…
siegfriedweber Apr 2, 2026
6432058
chore: Restructure code
siegfriedweber Apr 2, 2026
c1a4906
chore: Fix cargo-deny warning
siegfriedweber Apr 2, 2026
148d2ee
Update docs/modules/commons-operator/pages/restarter.adoc
siegfriedweber Apr 13, 2026
2c49aa6
chore: Fix changelog
siegfriedweber Apr 13, 2026
195a67d
Merge branch 'main' into feat/ignore-restarter
siegfriedweber Apr 13, 2026
77c9416
Update rust/operator-binary/src/restart_controller/statefulset.rs
siegfriedweber Apr 13, 2026
ceac434
Merge branch 'feat/ignore-restarter' of github.com:stackabletech/comm…
siegfriedweber Apr 13, 2026
74b5ac7
chore: Ignore RUSTSEC-2026-0097 in deny.toml
siegfriedweber Apr 15, 2026
dcdad6f
chore: Upgrade stackable-operator to version 0.110.0
siegfriedweber Apr 15, 2026
85069ce
chore: Update changelog
siegfriedweber Apr 15, 2026
46a8275
chore: Update deny.toml
siegfriedweber Apr 15, 2026
a334e4d
Merge branch 'main' into feat/ignore-restarter
siegfriedweber Apr 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Support the label `restarter.stackable.tech/ignore` on ConfigMaps and Secrets and the annotations
`restarter.stackable.tech/ignore-configmap.x` and `restarter.stackable.tech/ignore-secret.x` on
StatefulSets to exclude ConfigMaps and Secrets from the restarter controller ([#410]).

### Changed

- Document Helm deployed RBAC permissions and remove unnecessary permissions ([#412]).

[#410]: https://github.com/stackabletech/commons-operator/pull/410
[#412]: https://github.com/stackabletech/commons-operator/pull/412

## [26.3.0] - 2026-03-16
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resolver = "2"
version = "0.0.0-dev"
authors = ["Stackable GmbH <info@stackable.tech>"]
license = "OSL-3.0"
edition = "2021"
edition = "2024"
Comment thread
siegfriedweber marked this conversation as resolved.
repository = "https://github.com/stackabletech/commons-operator"

[workspace.dependencies]
Expand Down
58 changes: 58 additions & 0 deletions docs/modules/commons-operator/pages/restarter.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,61 @@ Label:: `restarter.stackable.tech/enabled`

The operator can restart StatefulSets when any referenced configuration object (ConfigMap or Secret) changes.
To enable this, set the `restarter.stackable.tech/enabled` label on the StatefulSet to `true`.

Annotation:: `restarter.stackable.tech/ignore-configmap.*`
Annotation:: `restarter.stackable.tech/ignore-secret.*`

These annotations can be added if the restarter is enabled on a StatefulSet, but some ConfigMaps or Secrets should be excluded from triggering a restart. `*` can be replaced with any value and is only used to make the annotation key unique.

[source,yaml]
----
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: statefulset-with-enabled-restarter
labels:
restarter.stackable.tech/enabled: "true"
annotations:
restarter.stackable.tech/ignore-configmap.0: hot-reloaded-configmap
restarter.stackable.tech/ignore-secret.0: hot-reloaded-secret
spec:
template:
spec:
volumes:
- name: configuration
configMap:
name: hot-reloaded-configmap
- name: credentials
secret:
secretName: hot-reloaded-secret
...
----

== ConfigMap/Secret

Label:: `restarter.stackable.tech/ignore`

If a ConfigMap or Secret is only used for initializing a StatefulSet or contains data which can be hot-reloaded, add the label `restarter.stackable.tech/ignore: "true"` to avoid unnecessary restarts of the StatefulSet Pods:

[source,yaml]
----
---
apiVersion: v1
kind: ConfigMap
metadata:
name: hot-reloaded-configmap
labels:
restarter.stackable.tech/ignore: "true"
...
---
apiVersion: v1
kind: Secret
metadata:
name: hot-reloaded-secret
labels:
restarter.stackable.tech/ignore: "true"
...
----

Unlike the StatefulSet annotations `restarter.stackable.tech/ignore-configmap.\*` and `restarter.stackable.tech/ignore-secret.*`, this label affects every StatefulSet that references the labeled ConfigMaps or Secrets.
23 changes: 10 additions & 13 deletions rust/operator-binary/src/restart_controller/pod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,23 +220,20 @@ async fn report_result(
const EVICT_ERROR_MESSAGE: &str =
"Cannot evict pod as it would violate the pod's disruption budget.";

// TODO: We need Rust 1.88 and 2024 edition for if-let-chains
if let kube::Error::Api(s) = evict_pod_error {
if let Status {
if let kube::Error::Api(s) = evict_pod_error
&& let Status {
code: TOO_MANY_REQUESTS_HTTP_CODE,
message: error_message,
..
} = s.deref()
{
if error_message == EVICT_ERROR_MESSAGE {
tracing::info!(
k8s.object.ref = %pod_ref,
error = %evict_pod_error,
"Tried to evict Pod, but wasn't allowed to do so, as it would violate the Pod's disruption budget. Retrying later"
);
return;
}
}
&& error_message == EVICT_ERROR_MESSAGE
{
tracing::info!(
k8s.object.ref = %pod_ref,
error = %evict_pod_error,
"Tried to evict Pod, but wasn't allowed to do so, as it would violate the Pod's disruption budget. Retrying later"
);
return;
}
}

Expand Down
118 changes: 83 additions & 35 deletions rust/operator-binary/src/restart_controller/statefulset.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::{collections::BTreeMap, future::Future, sync::Arc, time::Duration};
use std::{
collections::{BTreeMap, BTreeSet},
future::Future,
sync::Arc,
time::Duration,
};

use futures::{Stream, StreamExt, TryStream, stream};
use serde_json::json;
Expand Down Expand Up @@ -128,13 +133,20 @@ pub async fn start<F>(
trigger_all(
{
let cm_reader = cm_store.as_reader();
reflector(cm_store, metadata_watcher(cms, watcher::Config::default()))
.inspect(move |_| {
if let Some(tx) = cm_store_tx.take() {
tx.init(cm_reader.clone());
}
})
.touched_objects()
reflector(
cm_store,
metadata_watcher(
cms,
watcher::Config::default()
.labels("restarter.stackable.tech/ignore != true"),
Comment thread
sbernauer marked this conversation as resolved.
),
)
.inspect(move |_| {
if let Some(tx) = cm_store_tx.take() {
tx.init(cm_reader.clone());
}
})
.touched_objects()
},
sts_store.as_reader(),
),
Expand All @@ -143,7 +155,11 @@ pub async fn start<F>(
let secret_reader = secret_store.as_reader();
reflector(
secret_store,
metadata_watcher(secrets, watcher::Config::default()),
metadata_watcher(
secrets,
watcher::Config::default()
.labels("restarter.stackable.tech/ignore != true"),
),
)
.inspect(move |_| {
if let Some(tx) = secret_store_tx.take() {
Expand Down Expand Up @@ -235,11 +251,13 @@ pub async fn get_updated_restarter_annotations(
let ns = sts.metadata.namespace.as_deref().expect(
"A StatefulSet observed by a reflector (so send by Kubernetes) always has a namespace set",
);

let mut annotations = BTreeMap::<String, String>::new();
let pod_specs = sts
.spec
.iter()
.flat_map(|sts_spec| sts_spec.template.spec.as_ref());

let cm_refs = pod_specs
.clone()
.flat_map(|pod_spec| {
Expand Down Expand Up @@ -269,19 +287,35 @@ pub async fn get_updated_restarter_annotations(
})
.map(|cm_ref| cm_ref.within(ns));
let cms = ctx.cms.get().await.context(ConfigMapsUninitializedSnafu)?;
annotations.extend(cm_refs.flat_map(|cm_ref| cms.get(&cm_ref)).flat_map(|cm| {
Some((
format!(
"configmap.restarter.stackable.tech/{}",
cm.metadata.name.as_ref()?
),
format!(
"{}/{}",
cm.metadata.uid.as_ref()?,
cm.metadata.resource_version.as_ref()?
),
))
}));
let ignored_cms = sts
.metadata
.annotations
.iter()
.flatten()
.filter_map(|(key, value)| {
key.starts_with("restarter.stackable.tech/ignore-configmap.")
.then_some(value)
})
.collect::<BTreeSet<_>>();
annotations.extend(
cm_refs
.map(|cm_ref| (cm_ref.name.clone(), cms.get(&cm_ref)))
.map(|(cm_name, cm)| {
(
format!("configmap.restarter.stackable.tech/{cm_name}",),
if let Some(cm) = cm
&& let Some(uid) = &cm.metadata.uid
&& let Some(resource_version) = &cm.metadata.resource_version
&& !ignored_cms.contains(&cm_name)
{
format!("{uid}/{resource_version}",)
} else {
"changes-ignored".to_owned()
},
)
}),
);

let secret_refs = pod_specs
.flat_map(|pod_spec| {
find_pod_refs(
Expand All @@ -305,23 +339,37 @@ pub async fn get_updated_restarter_annotations(
})
.map(|secret_ref| secret_ref.within(ns));
let secrets = ctx.secrets.get().await.context(SecretsUninitializedSnafu)?;
let ignored_secrets = sts
.metadata
.annotations
.iter()
.flatten()
.filter(|annotation| {
annotation
.0
.starts_with("restarter.stackable.tech/ignore-secret.")
})
.map(|x| x.1)
.collect::<BTreeSet<_>>();
annotations.extend(
secret_refs
.flat_map(|secret_ref| secrets.get(&secret_ref))
.flat_map(|cm| {
Some((
format!(
"secret.restarter.stackable.tech/{}",
cm.metadata.name.as_ref()?
),
format!(
"{}/{}",
cm.metadata.uid.as_ref()?,
cm.metadata.resource_version.as_ref()?
),
))
.map(|secret_ref| (secret_ref.name.clone(), secrets.get(&secret_ref)))
.map(|(secret_name, secret)| {
(
format!("secret.restarter.stackable.tech/{secret_name}",),
if let Some(secret) = secret
&& let Some(uid) = &secret.metadata.uid
&& let Some(resource_version) = &secret.metadata.resource_version
&& !ignored_secrets.contains(&secret_name)
{
format!("{uid}/{resource_version}",)
} else {
"changes-ignored".to_owned()
},
)
}),
);

Ok(annotations)
}

Expand Down
4 changes: 2 additions & 2 deletions tests/templates/kuttl/restarter/10-assert.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
timeout: 30
timeout: 120
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sleep
name: test
status:
readyReplicas: 1
replicas: 1
Loading
Loading