Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e8a291d
add agent label utility
labrenbe Oct 1, 2026
6ca45a0
add TlsClientCredential enum as platform access utility
labrenbe Oct 1, 2026
7cb677e
add changelog entry
labrenbe Oct 1, 2026
7a0d956
fix cargo deny by upgrading yoke-derive
labrenbe Oct 1, 2026
f3d2da7
use hard-coded volume and mount names
labrenbe Oct 1, 2026
3b91d73
add support for anonymous platform access
labrenbe Oct 6, 2026
e364acd
make crd fragments versioned
labrenbe Oct 6, 2026
7687938
add ManagementPolicy CRD fragment
labrenbe Oct 7, 2026
754e444
add Ready & Synced conditions for resources reconciled by agents
labrenbe Oct 7, 2026
92e1a36
rename tls client cert dir
labrenbe Oct 14, 2026
88e3184
Merge remote-tracking branch 'origin/main' into feat/platform-access
labrenbe Oct 23, 2026
e9c595e
harmonize CRD fragments with accepted decisions
labrenbe Oct 23, 2026
a575fc3
move managed resource conditions to status and support Unknown and ob…
labrenbe Oct 23, 2026
d67261e
Merge remote-tracking branch 'origin/main' into feat/platform-access
labrenbe Oct 23, 2026
22536cd
avoid clashes with role names in component label
labrenbe Oct 23, 2026
0f8cbeb
return an error when the TLS client certificate volume collides
labrenbe Oct 23, 2026
1ddd9fc
use with_secret
labrenbe Oct 23, 2026
a2a7d0c
make the description of anonymous idependent from agent
labrenbe Oct 23, 2026
079f741
add RbacResourceNames trait and AgentResourceNames type
labrenbe Oct 26, 2026
1247d14
add the stackable-agent crate with DeletionPolicy and managed resourc…
labrenbe Oct 26, 2026
8eae8f0
Merge branch 'feat/stackable-agent' into feat/platform-access
labrenbe Oct 26, 2026
b815f20
add ResourceOperation to stackable-agent
labrenbe Oct 26, 2026
ccfff58
add all_agents_selector util
labrenbe Oct 27, 2026
a46d940
change doc comment on ManagedCondition reason
labrenbe Oct 28, 2026
63760c6
add autoTLS domain components to agent tls cert
labrenbe Oct 28, 2026
b61f619
remove autoTLS domain components to agent tls cert again
labrenbe Oct 28, 2026
d769774
Merge remote-tracking branch 'origin/main' into feat/platform-access
sbernauer Oct 29, 2026
8adb4da
add CRD preview
labrenbe Oct 29, 2026
46a18a9
derive the condition status strings
labrenbe Oct 29, 2026
02db679
Merge remote-tracking branch 'origin/main' into feat/platform-access
labrenbe Oct 30, 2026
e77a2b1
fix changelogs
labrenbe Nov 2, 2026
915f8c8
Merge remote-tracking branch 'origin/main' into feat/platform-access
labrenbe Nov 2, 2026
2be5aec
fix prek
labrenbe Nov 2, 2026
1d2bf3f
fix: Temporarily allow redundant_field_names lint
Techassi Nov 2, 2026
fe34baf
chore: Fix various Clippy errors
Techassi Nov 2, 2026
bc7e95f
chore: Add link to darling fix PR
Techassi Nov 2, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Please see the relevant crate changelogs:
- [config-utils](./crates/config-utils/CHANGELOG.md)
- [containerdebug](./crates/containerdebug/CHANGELOG.md)
- [k8s-version](./crates/k8s-version/CHANGELOG.md)
- [stackable-agent](./crates/stackable-agent/CHANGELOG.md)
- [stackable-certs](./crates/stackable-certs/CHANGELOG.md)
- [stackable-operator](./crates/stackable-operator/CHANGELOG.md)
- [stackable-operator-derive](./crates/stackable-operator-derive/CHANGELOG.md)
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ zeroize = "1.9.1"
# Enable all pedantic lints (with lower priority so individual lints can override)
pedantic = { level = "deny", priority = -1 }

# NOTE (@Techassi): Since Rust 1.99.0 this lint is triggered by fields which use `#[darling(multiple)]`.
# This seems to be a false-positive as far as I can see. Allowing this lint at the container or
# field level did not work. That's why it is now allowed globally.
# TODO (@Techassi): Remove this once the fix (see https://github.com/TedDriggs/darling/pull/441) is
# released.
redundant_field_names = "allow"

# Pedantic lints we don't enforce (yet)
doc_markdown = "allow"
missing_errors_doc = "allow"
Expand Down
21 changes: 21 additions & 0 deletions crates/stackable-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- `DeletionPolicy`, whether an agent deletes a resource in the product ([#1290]).
- `ResourceOperation` with `reconciliationPaused`, which stops an agent from changing a resource
in the product ([#1290]).
- `Ready`/`Synced` conditions of resources managed by agents (`condition`), as `metav1.Condition`
with `True`/`False`/`Unknown` and `observedGeneration` ([#1290]).

## [0.0.1]

### Added

- Add the `stackable-agent` crate as a workspace member ([#1290]).

[#1290]: https://github.com/stackabletech/operator-rs/pull/1290
23 changes: 23 additions & 0 deletions crates/stackable-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "stackable-agent"
description = "Building blocks for agents managing resources inside products of the Stackable Data Platform"
version = "0.0.1"
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true

[dependencies]
stackable-versioned = { path = "../stackable-versioned" }

k8s-openapi.workspace = true
schemars.workspace = true
serde.workspace = true
strum.workspace = true

[dev-dependencies]
rstest.workspace = true
serde_json.workspace = true

[lints]
workspace = true
186 changes: 186 additions & 0 deletions crates/stackable-agent/src/condition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//! Conditions of resources an agent manages inside a product, as `metav1.Condition`.

use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
use strum::Display;

/// Condition types of resources an agent manages inside a product.
#[derive(Clone, Copy, Debug, Display, Eq, PartialEq)]
pub enum ManagedConditionType {
/// The resource exists in the product and is usable.
Ready,

/// The last reconciliation applied the spec to the resource in the product.
Synced,
}

/// The status of a condition.
#[derive(Clone, Copy, Debug, Display, Eq, PartialEq)]
pub enum ConditionStatus {
True,
False,
Unknown,
}

/// The desired state of one condition, without the bookkeeping fields.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManagedCondition {
Comment thread
maltesander marked this conversation as resolved.
Resolved
pub type_: ManagedConditionType,

/// `Unknown` if the agent could not determine the state, e.g. because the product is not
/// reachable.
pub status: ConditionStatus,

/// A CamelCase reason like `Available` or `UnsupportedChange`.
pub reason: String,

pub message: String,
}

/// Merges the given conditions into the current ones.
///
/// `lastTransitionTime` only changes if the status of a condition changes. Conditions of other
/// types are kept. `observed_generation` is the `metadata.generation` of the object the conditions
/// were determined for.
pub fn update_conditions(
current: &[Condition],
updates: Vec<ManagedCondition>,
now: &Time,
observed_generation: Option<i64>,
) -> Vec<Condition> {
let mut conditions = current.to_vec();

for update in updates {
let type_ = update.type_.to_string();
let status = update.status.to_string();
let existing = conditions
.iter()
.position(|condition| condition.type_ == type_);

let last_transition_time = match existing {
Some(index) if conditions[index].status == status => {
conditions[index].last_transition_time.clone()
}
_ => now.clone(),
};

let condition = Condition {
last_transition_time,
message: update.message,
observed_generation,
reason: update.reason,
status,
type_,
};

match existing {
Some(index) => conditions[index] = condition,
None => conditions.push(condition),
}
}

conditions
}

#[cfg(test)]
mod tests {
use k8s_openapi::jiff::Timestamp;

use super::*;

fn time(seconds: i64) -> Time {
Time(Timestamp::from_second(seconds).expect("the timestamp should be valid"))
}

fn ready(status: ConditionStatus, reason: &str) -> ManagedCondition {
ManagedCondition {
type_: ManagedConditionType::Ready,
status,
reason: reason.to_owned(),
message: String::new(),
}
}

#[test]
fn add_missing_condition() {
let conditions = update_conditions(
&[],
vec![ready(ConditionStatus::True, "Created")],
&time(10),
Some(3),
);

assert_eq!(1, conditions.len());
assert_eq!("Ready", conditions[0].type_);
assert_eq!("True", conditions[0].status);
assert_eq!("Created", conditions[0].reason);
assert_eq!(time(10), conditions[0].last_transition_time);
assert_eq!(Some(3), conditions[0].observed_generation);
}

#[test]
fn keep_transition_time_if_status_is_unchanged() {
let current = update_conditions(
&[],
vec![ready(ConditionStatus::True, "Created")],
&time(10),
Some(1),
);

let conditions = update_conditions(
&current,
vec![ready(ConditionStatus::True, "Updated")],
&time(20),
Some(2),
);

assert_eq!("Updated", conditions[0].reason);
assert_eq!(time(10), conditions[0].last_transition_time);
assert_eq!(Some(2), conditions[0].observed_generation);
}

#[test]
fn update_transition_time_if_status_changes() {
let current = update_conditions(
&[],
vec![ready(ConditionStatus::True, "Created")],
&time(10),
Some(1),
);

let conditions = update_conditions(
&current,
vec![ready(ConditionStatus::Unknown, "ConnectionFailed")],
&time(20),
Some(1),
);

assert_eq!("Unknown", conditions[0].status);
assert_eq!(time(20), conditions[0].last_transition_time);
}

#[test]
fn keep_conditions_of_other_types() {
let other = Condition {
last_transition_time: time(5),
message: String::new(),
observed_generation: None,
reason: "Other".to_owned(),
status: "True".to_owned(),
type_: "Other".to_owned(),
};

let conditions = update_conditions(
std::slice::from_ref(&other),
vec![ready(ConditionStatus::True, "Created")],
&time(10),
Some(1),
);

let types: Vec<_> = conditions
.iter()
.map(|condition| condition.type_.as_str())
.collect();
assert_eq!(vec!["Other", "Ready"], types);
assert_eq!(None, conditions[0].observed_generation);
}
}
44 changes: 44 additions & 0 deletions crates/stackable-agent/src/deletion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use stackable_versioned::versioned;

#[versioned(version(name = "v1alpha1"))]
pub mod versioned {
/// What happens to a resource in the product when the object managing it is deleted.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub enum DeletionPolicy {
/// Keep the resource in the product.
#[default]
Retain,

/// Delete the resource in the product.
Delete,
Comment thread
labrenbe marked this conversation as resolved.
Resolved
}
}

#[cfg(test)]
mod tests {
use rstest::rstest;

use super::v1alpha1::DeletionPolicy;

#[rstest]
#[case::retain(r#""Retain""#, DeletionPolicy::Retain)]
#[case::delete(r#""Delete""#, DeletionPolicy::Delete)]
fn deserialize_policy(#[case] input: &str, #[case] expected: DeletionPolicy) {
let policy: DeletionPolicy =
serde_json::from_str(input).expect("the policy should be valid");

assert_eq!(expected, policy);
}

#[test]
fn retain_by_default() {
assert_eq!(DeletionPolicy::Retain, DeletionPolicy::default());
}

#[test]
fn reject_unknown_policy() {
assert!(serde_json::from_str::<DeletionPolicy>(r#""Orphan""#).is_err());
}
}
5 changes: 5 additions & 0 deletions crates/stackable-agent/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Building blocks for agents managing resources inside products, like topics in Apache Kafka.

pub mod condition;
pub mod deletion;
pub mod operation;
38 changes: 38 additions & 0 deletions crates/stackable-agent/src/operation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use stackable_versioned::versioned;

#[versioned(version(name = "v1alpha1"))]
pub mod versioned {
/// Operations on this resource in the product, independent of the `clusterOperation` of the
/// product cluster.
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceOperation {
/// Stops the agent from changing the resource in the product. The status is still updated.
#[serde(default)]
pub reconciliation_paused: bool,
}
}
Comment thread
maltesander marked this conversation as resolved.
Resolved

#[cfg(test)]
mod tests {
use super::v1alpha1::ResourceOperation;

#[test]
fn not_paused_by_default() {
let operation: ResourceOperation =
serde_json::from_str("{}").expect("the operation should be valid");

assert!(!operation.reconciliation_paused);
}

#[test]
fn deserialize_paused() {
let operation: ResourceOperation =
serde_json::from_str(r#"{"reconciliationPaused": true}"#)
.expect("the operation should be valid");

assert!(operation.reconciliation_paused);
}
}
Loading
Loading