Skip to content

Commit e58f921

Browse files
feat: Support configuring ListenerClass preset (with sensible defaults) (#414)
* feat: Support configuring listner-operator preset (with sensible defaults) * Update rustdoc * changelog * --listener-operator-preset -> --listener-class-presets * Update rust/stackable-cockpit/src/platform/operator/listener_operator.rs Co-authored-by: Nick <[email protected]> * Apply suggestions from code review Co-authored-by: Nick <[email protected]> * Fix compilation * Make CLI flag --listener-class-preset singular * Update CLI docs * change link title in changelog * Update rust/stackablectl/src/args/operator_configs.rs Co-authored-by: Nick <[email protected]> * Remove docs on temporary * Update rust/stackable-cockpit/src/platform/operator/mod.rs Co-authored-by: Nick <[email protected]> * Update rust/stackablectl/CHANGELOG.md Co-authored-by: Nick <[email protected]> --------- Co-authored-by: Nick <[email protected]>
1 parent a31686d commit e58f921

File tree

8 files changed

+159
-7
lines changed

8 files changed

+159
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/stackable-cockpit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ openapi = ["dep:utoipa"]
1616
helm-sys = { path = "../helm-sys" }
1717

1818
bcrypt.workspace = true
19+
clap.workspace = true
1920
indexmap.workspace = true
2021
k8s-openapi.workspace = true
2122
kube.workspace = true
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use clap::ValueEnum;
2+
use snafu::ResultExt;
3+
use stackable_operator::{
4+
k8s_openapi::api::core::v1::Node,
5+
kube::{Api, Client, api::ListParams},
6+
};
7+
use tokio::sync::OnceCell;
8+
use tracing::{debug, info, instrument};
9+
10+
pub static LISTENER_CLASS_PRESET: OnceCell<ListenerClassPreset> = OnceCell::const_new();
11+
12+
/// Represents the `preset` value in the Listener Operator Helm Chart
13+
#[derive(Copy, Clone, Debug, ValueEnum)]
14+
pub enum ListenerClassPreset {
15+
None,
16+
StableNodes,
17+
EphemeralNodes,
18+
}
19+
20+
impl ListenerClassPreset {
21+
pub fn as_helm_values(&self) -> String {
22+
let preset_value = match self {
23+
Self::None => "none",
24+
Self::StableNodes => "stable-nodes",
25+
Self::EphemeralNodes => "ephemeral-nodes",
26+
};
27+
format!("preset: {preset_value}")
28+
}
29+
}
30+
31+
#[instrument]
32+
pub async fn determine_and_store_listener_class_preset(from_cli: Option<&ListenerClassPreset>) {
33+
if let Some(from_cli) = from_cli {
34+
LISTENER_CLASS_PRESET
35+
.set(*from_cli)
36+
.expect("LISTENER_CLASS_PRESET should be unset");
37+
return;
38+
}
39+
40+
let kubernetes_environment = guess_kubernetes_environment().await.unwrap_or_else(|err| {
41+
info!("failed to determine Kubernetes environment, using defaults: {err:#?}");
42+
KubernetesEnvironment::Unknown
43+
});
44+
let listener_class_preset = match kubernetes_environment {
45+
// Kind does not support LoadBalancers out of the box, so avoid that
46+
KubernetesEnvironment::Kind => ListenerClassPreset::StableNodes,
47+
// LoadBalancer support in k3s is optional, so let's be better safe than sorry and not use
48+
// them
49+
KubernetesEnvironment::K3s => ListenerClassPreset::StableNodes,
50+
// Weekly node rotations and LoadBalancer support
51+
KubernetesEnvironment::Ionos => ListenerClassPreset::EphemeralNodes,
52+
// Don't pin nodes and assume we have LoadBalancer support
53+
KubernetesEnvironment::Unknown => ListenerClassPreset::EphemeralNodes,
54+
};
55+
debug!(
56+
preset = ?listener_class_preset,
57+
kubernetes.environment = ?kubernetes_environment,
58+
"Using ListenerClass preset"
59+
);
60+
61+
LISTENER_CLASS_PRESET
62+
.set(listener_class_preset)
63+
.expect("LISTENER_CLASS_PRESET should be unset");
64+
}
65+
66+
#[derive(Debug)]
67+
enum KubernetesEnvironment {
68+
Kind,
69+
K3s,
70+
Ionos,
71+
Unknown,
72+
}
73+
74+
/// Tries to guess what Kubernetes environment stackablectl is connecting to.
75+
///
76+
/// Returns an error in case anything goes wrong. This could e.g. be the case in case no
77+
/// Kubernetes context is configured, stackablectl is missing RBAC permission to retrieve nodes or
78+
/// simply a network error.
79+
#[instrument]
80+
async fn guess_kubernetes_environment() -> Result<KubernetesEnvironment, snafu::Whatever> {
81+
let client = Client::try_default()
82+
.await
83+
.whatever_context("failed to construct Kubernetes client")?;
84+
let node_api: Api<Node> = Api::all(client);
85+
let nodes = node_api
86+
.list(&ListParams::default())
87+
.await
88+
.whatever_context("failed to list Kubernetes nodes")?;
89+
90+
for node in nodes {
91+
if let Some(spec) = node.spec {
92+
if let Some(provider_id) = spec.provider_id {
93+
if provider_id.starts_with("kind://") {
94+
return Ok(KubernetesEnvironment::Kind);
95+
} else if provider_id.starts_with("k3s://") {
96+
return Ok(KubernetesEnvironment::K3s);
97+
} else if provider_id.starts_with("ionos://") {
98+
return Ok(KubernetesEnvironment::Ionos);
99+
}
100+
}
101+
}
102+
}
103+
104+
Ok(KubernetesEnvironment::Unknown)
105+
}

rust/stackable-cockpit/src/platform/operator/mod.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{fmt::Display, str::FromStr};
22

3+
use listener_operator::LISTENER_CLASS_PRESET;
34
use semver::Version;
45
use serde::Serialize;
56
use snafu::{ResultExt, Snafu, ensure};
@@ -14,6 +15,8 @@ use crate::{
1415
utils::operator_chart_name,
1516
};
1617

18+
pub mod listener_operator;
19+
1720
pub const VALID_OPERATORS: &[&str] = &[
1821
"airflow",
1922
"commons",
@@ -93,10 +96,9 @@ impl FromStr for OperatorSpec {
9396
ensure!(len <= 2, InvalidEqualSignCountSnafu);
9497

9598
// Check if the provided operator name is in the list of valid operators
96-
ensure!(
97-
VALID_OPERATORS.contains(&parts[0]),
98-
InvalidNameSnafu { name: parts[0] }
99-
);
99+
ensure!(VALID_OPERATORS.contains(&parts[0]), InvalidNameSnafu {
100+
name: parts[0]
101+
});
100102

101103
// If there is only one part, the input didn't include
102104
// the optional version identifier
@@ -208,6 +210,15 @@ impl OperatorSpec {
208210
ChartSourceType::Repo => self.helm_repo_name(),
209211
};
210212

213+
let mut helm_values = None;
214+
if self.name == "listener" {
215+
helm_values = Some(
216+
LISTENER_CLASS_PRESET.get()
217+
.expect("At this point LISTENER_CLASS_PRESET must be set by determine_and_store_listener_class_preset")
218+
.as_helm_values()
219+
);
220+
};
221+
211222
// Install using Helm
212223
helm::install_release_from_repo_or_registry(
213224
&helm_name,
@@ -216,7 +227,7 @@ impl OperatorSpec {
216227
chart_name: &helm_name,
217228
chart_source: &chart_source,
218229
},
219-
None,
230+
helm_values.as_deref(),
220231
namespace,
221232
true,
222233
)?;

rust/stackablectl/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Automatically detect Kubernetes environment (e.g. kind, k3s or IONOS) and choose a sensible [ListenerClass preset] by default ([#414]).
10+
- Support configuring the [ListenerClass preset] using `--listener-class-preset` ([#414]).
11+
12+
[#414]: https://github.com/stackabletech/stackable-cockpit/pull/414
13+
[ListenerClass preset]: https://docs.stackable.tech/home/nightly/listener-operator/listenerclass/#presets
14+
715
## [1.1.0] - 2025-07-16
816

917
### Added

rust/stackablectl/src/args/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod cluster;
22
mod file;
33
mod namespace;
4+
mod operator_configs;
45
mod repo;
56

67
pub use cluster::*;
78
pub use file::*;
89
pub use namespace::*;
10+
pub use operator_configs::*;
911
pub use repo::*;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use clap::Args;
2+
use stackable_cockpit::platform::operator::listener_operator::ListenerClassPreset;
3+
4+
#[derive(Debug, Args)]
5+
#[command(next_help_heading = "Operator specific configurations")]
6+
pub struct CommonOperatorConfigsArgs {
7+
/// Choose the ListenerClass preset (`none`, `ephemeral-nodes` or `stable-nodes`).
8+
///
9+
/// This maps to the listener-operator Helm Chart preset value, see
10+
/// [the listener-operator documentation](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass/#presets)
11+
/// for details.
12+
#[arg(long, global = true)]
13+
pub listener_class_preset: Option<ListenerClassPreset>,
14+
}

rust/stackablectl/src/cli/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use snafu::{ResultExt, Snafu};
66
use stackable_cockpit::{
77
constants::{HELM_REPO_NAME_DEV, HELM_REPO_NAME_STABLE, HELM_REPO_NAME_TEST},
88
helm,
9-
platform::operator::ChartSourceType,
9+
platform::operator::{
10+
ChartSourceType, listener_operator::determine_and_store_listener_class_preset,
11+
},
1012
utils::path::{
1113
IntoPathOrUrl, IntoPathsOrUrls, ParsePathsOrUrls, PathOrUrl, PathOrUrlParseError,
1214
},
@@ -15,7 +17,7 @@ use stackable_cockpit::{
1517
use tracing::{Level, instrument};
1618

1719
use crate::{
18-
args::{CommonFileArgs, CommonRepoArgs},
20+
args::{CommonFileArgs, CommonOperatorConfigsArgs, CommonRepoArgs},
1921
cmds::{cache, completions, debug, demo, operator, release, stack, stacklet},
2022
constants::{
2123
DEMOS_REPOSITORY_DEMOS_SUBPATH, DEMOS_REPOSITORY_STACKS_SUBPATH, DEMOS_REPOSITORY_URL_BASE,
@@ -79,6 +81,9 @@ Cached files are saved at '$XDG_CACHE_HOME/stackablectl', which is usually
7981
#[command(flatten)]
8082
pub repos: CommonRepoArgs,
8183

84+
#[command(flatten)]
85+
pub operator_configs: CommonOperatorConfigsArgs,
86+
8287
#[command(subcommand)]
8388
pub subcommand: Commands,
8489
}
@@ -186,6 +191,11 @@ impl Cli {
186191
// TODO (Techassi): Do we still want to auto purge when running cache commands?
187192
cache.auto_purge().await.unwrap();
188193

194+
determine_and_store_listener_class_preset(
195+
self.operator_configs.listener_class_preset.as_ref(),
196+
)
197+
.await;
198+
189199
match &self.subcommand {
190200
Commands::Operator(args) => args.run(self).await.context(OperatorSnafu),
191201
Commands::Release(args) => args.run(self, cache).await.context(ReleaseSnafu),

0 commit comments

Comments
 (0)