Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions rust/stackable-cockpit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ openapi = ["dep:utoipa"]
helm-sys = { path = "../helm-sys" }

bcrypt.workspace = true
clap.workspace = true
indexmap.workspace = true
k8s-openapi.workspace = true
kube.workspace = true
Expand Down
106 changes: 106 additions & 0 deletions rust/stackable-cockpit/src/platform/operator/listener_operator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use clap::ValueEnum;
use snafu::ResultExt;
use stackable_operator::{
k8s_openapi::api::core::v1::Node,
kube::{Api, Client, api::ListParams},
};
use tokio::sync::OnceCell;
use tracing::{debug, info, instrument};

pub static LISTENER_OPERATOR_PRESET: OnceCell<ListenerOperatorPreset> = OnceCell::const_new();
Comment thread
sbernauer marked this conversation as resolved.
Outdated

#[derive(Copy, Clone, Debug, ValueEnum)]
Comment thread
sbernauer marked this conversation as resolved.
pub enum ListenerOperatorPreset {
Comment thread
sbernauer marked this conversation as resolved.
Outdated
None,
StableNodes,
EphemeralNodes,
}

impl ListenerOperatorPreset {
pub fn as_helm_values(&self) -> String {
let preset_value = match self {
Self::None => "none",
Self::StableNodes => "stable-nodes",
Self::EphemeralNodes => "ephemeral-nodes",
};
format!("preset: {preset_value}")
}
}

#[instrument]
pub async fn determine_and_store_listener_operator_preset(
from_cli: Option<&ListenerOperatorPreset>,
Comment thread
sbernauer marked this conversation as resolved.
Outdated
) {
if let Some(from_cli) = from_cli {
LISTENER_OPERATOR_PRESET
.set(*from_cli)
.expect("We are the only function setting LISTENER_OPERATOR_PRESET");
Comment thread
sbernauer marked this conversation as resolved.
Outdated
return;
}

let kubernetes_environment = guess_kubernetes_environment().await.unwrap_or_else(|err| {
info!("failed to determine Kubernetes environment, using defaults: {err:#?}");
KubernetesEnvironment::Unknown
});
let listener_operator_preset = match kubernetes_environment {
// Kind does not support LoadBalancers out of the box, so avoid that
KubernetesEnvironment::Kind => ListenerOperatorPreset::StableNodes,
// LoadBalancer support in k3s is optional, so let's be better safe than sorry and not use
// them
KubernetesEnvironment::K3s => ListenerOperatorPreset::StableNodes,
// Weekly node rotations and LoadBalancer support
KubernetesEnvironment::Ionos => ListenerOperatorPreset::EphemeralNodes,
// Don't pin nodes and assume we have LoadBalancer support
KubernetesEnvironment::Unknown => ListenerOperatorPreset::EphemeralNodes,
};
debug!(
preset = ?listener_operator_preset,
kubernetes.environment = ?kubernetes_environment,
"Using listener-operator preset"
);

LISTENER_OPERATOR_PRESET
.set(listener_operator_preset)
Comment thread
sbernauer marked this conversation as resolved.
Outdated
.expect("We are the only function setting LISTENER_OPERATOR_PRESET");
Comment thread
sbernauer marked this conversation as resolved.
Outdated
}

#[derive(Debug)]
enum KubernetesEnvironment {
Kind,
K3s,
Ionos,
Unknown,
}

/// Tries to guess what Kubernetes environment stackablectl is connecting to.
///
/// Returns an error in case anything goes wrong. This could e.g. be the case in case no
/// Kubernetes context is configured, stackablectl is missing RBAC permission to retrieve nodes or
/// simply a network error.
#[instrument]
async fn guess_kubernetes_environment() -> Result<KubernetesEnvironment, snafu::Whatever> {
let client = Client::try_default()
.await
.whatever_context("failed to construct Kubernetes client")?;
let node_api: Api<Node> = Api::all(client);
let nodes = node_api
.list(&ListParams::default())
.await
.whatever_context("failed to list Kubernetes nodes")?;

for node in nodes {
if let Some(spec) = node.spec {
if let Some(provider_id) = spec.provider_id {
if provider_id.starts_with("kind://") {
return Ok(KubernetesEnvironment::Kind);
} else if provider_id.starts_with("k3s://") {
return Ok(KubernetesEnvironment::K3s);
} else if provider_id.starts_with("ionos://") {
return Ok(KubernetesEnvironment::Ionos);
}
}
}
}

Ok(KubernetesEnvironment::Unknown)
}
21 changes: 16 additions & 5 deletions rust/stackable-cockpit/src/platform/operator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{fmt::Display, str::FromStr};

use listener_operator::LISTENER_OPERATOR_PRESET;
use semver::Version;
use serde::Serialize;
use snafu::{ResultExt, Snafu, ensure};
Expand All @@ -14,6 +15,8 @@ use crate::{
utils::operator_chart_name,
};

pub mod listener_operator;

pub const VALID_OPERATORS: &[&str] = &[
"airflow",
"commons",
Expand Down Expand Up @@ -93,10 +96,9 @@ impl FromStr for OperatorSpec {
ensure!(len <= 2, InvalidEqualSignCountSnafu);

// Check if the provided operator name is in the list of valid operators
ensure!(
VALID_OPERATORS.contains(&parts[0]),
InvalidNameSnafu { name: parts[0] }
);
ensure!(VALID_OPERATORS.contains(&parts[0]), InvalidNameSnafu {
name: parts[0]
});

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

let mut helm_values = None;
if self.name == "listener" {
helm_values = Some(
LISTENER_OPERATOR_PRESET.get()
.expect("At this point LISTENER_OPERATOR_PRESET must be set by determine_and_store_listener_operator_preset")
Comment thread
sbernauer marked this conversation as resolved.
Outdated
.as_helm_values()
);
};

// Install using Helm
helm::install_release_from_repo_or_registry(
&helm_name,
Expand All @@ -216,7 +227,7 @@ impl OperatorSpec {
chart_name: &helm_name,
chart_source: &chart_source,
},
None,
helm_values.as_deref(),
namespace,
true,
)?;
Expand Down
8 changes: 8 additions & 0 deletions rust/stackablectl/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Automatically detect Kubernetes environment (e.g. kind, k3s or IONOS) and choose a sensible [listener-operator preset] by default ([#414]).
Comment thread
sbernauer marked this conversation as resolved.
Outdated
- Support configuring the [listener-operator preset] using `--listener-class-presets` ([#414]).

[#414]: https://github.com/stackabletech/stackable-cockpit/pull/414
[listener-operator preset]: https://docs.stackable.tech/home/nightly/listener-operator/listenerclass/#presets
Comment thread
sbernauer marked this conversation as resolved.
Outdated

## [1.1.0] - 2025-07-16

### Added
Expand Down
2 changes: 2 additions & 0 deletions rust/stackablectl/src/args/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod cluster;
mod file;
mod namespace;
mod operator_configs;
mod repo;

pub use cluster::*;
pub use file::*;
pub use namespace::*;
pub use operator_configs::*;
pub use repo::*;
17 changes: 17 additions & 0 deletions rust/stackablectl/src/args/operator_configs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use clap::Args;
use stackable_cockpit::platform::operator::listener_operator::ListenerOperatorPreset;
Comment thread
sbernauer marked this conversation as resolved.
Outdated

#[derive(Debug, Args)]
#[command(next_help_heading = "Operator specific configurations")]
pub struct CommonOperatorConfigsArgs {
/// Choose the ListenerClass presets (`none`, `ephemeral-nodes` or `stable-nodes`).
///
/// This maps to the listener-operator preset, see
Comment thread
sbernauer marked this conversation as resolved.
Outdated
/// [the listener-operator documentation](https://docs.stackable.tech/home/nightly/listener-operator/listenerclass/#presets)
/// for details.
///
/// This argument is likely temporary until we support setting arbitrary helm values for the
/// operators!
Comment thread
NickLarsenNZ marked this conversation as resolved.
Outdated
#[arg(long, global = true)]
pub listener_class_presets: Option<ListenerOperatorPreset>,
Comment thread
sbernauer marked this conversation as resolved.
Outdated
}
14 changes: 12 additions & 2 deletions rust/stackablectl/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use snafu::{ResultExt, Snafu};
use stackable_cockpit::{
constants::{HELM_REPO_NAME_DEV, HELM_REPO_NAME_STABLE, HELM_REPO_NAME_TEST},
helm,
platform::operator::ChartSourceType,
platform::operator::{
ChartSourceType, listener_operator::determine_and_store_listener_operator_preset,
Comment thread
sbernauer marked this conversation as resolved.
Outdated
},
utils::path::{
IntoPathOrUrl, IntoPathsOrUrls, ParsePathsOrUrls, PathOrUrl, PathOrUrlParseError,
},
Expand All @@ -15,7 +17,7 @@ use stackable_cockpit::{
use tracing::{Level, instrument};

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

#[command(flatten)]
pub operator_configs: CommonOperatorConfigsArgs,

#[command(subcommand)]
pub subcommand: Commands,
}
Expand Down Expand Up @@ -186,6 +191,11 @@ impl Cli {
// TODO (Techassi): Do we still want to auto purge when running cache commands?
cache.auto_purge().await.unwrap();

determine_and_store_listener_operator_preset(
self.operator_configs.listener_class_presets.as_ref(),
Comment thread
sbernauer marked this conversation as resolved.
Outdated
)
.await;

match &self.subcommand {
Commands::Operator(args) => args.run(self).await.context(OperatorSnafu),
Commands::Release(args) => args.run(self, cache).await.context(ReleaseSnafu),
Expand Down
Loading