Skip to content

Commit b73d475

Browse files
committed
chore: Merge branch 'main' into feat/stackable-versioned-conversion-tracking
2 parents ec4005b + 7328943 commit b73d475

File tree

11 files changed

+444
-343
lines changed

11 files changed

+444
-343
lines changed

Cargo.lock

Lines changed: 243 additions & 297 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ educe = { version = "0.6.0", default-features = false, features = ["Clone", "De
2525
either = "1.13.0"
2626
futures = "0.3.30"
2727
futures-util = "0.3.30"
28+
http = "1.3.1"
2829
indexmap = "2.5.0"
2930
indoc = "2.0.6"
3031
insta = { version= "1.40", features = ["glob"] }
@@ -36,11 +37,11 @@ k8s-openapi = { version = "0.25.0", default-features = false, features = ["schem
3637
# We use rustls instead of openssl for easier portability, e.g. so that we can build stackablectl without the need to vendor (build from source) openssl
3738
# We use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev"
3839
kube = { version = "1.1.0", default-features = false, features = ["client", "jsonpatch", "runtime", "derive", "rustls-tls", "ring"] }
39-
opentelemetry = "0.29.1"
40-
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
41-
opentelemetry-appender-tracing = "0.29.1"
42-
opentelemetry-otlp = "0.29.0"
43-
opentelemetry-semantic-conventions = "0.29.0"
40+
opentelemetry = "0.30.0"
41+
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
42+
opentelemetry-appender-tracing = "0.30.1"
43+
opentelemetry-otlp = "0.30.0"
44+
opentelemetry-semantic-conventions = "0.30.0"
4445
p256 = { version = "0.13.2", features = ["ecdsa"] }
4546
paste = "1.0.15"
4647
pin-project = "1.1.5"
@@ -74,7 +75,7 @@ tower = { version = "0.5.1", features = ["util"] }
7475
tower-http = { version = "0.6.1", features = ["trace"] }
7576
tracing = "0.1.40"
7677
tracing-appender = "0.2.3"
77-
tracing-opentelemetry = "0.30.0"
78+
tracing-opentelemetry = "0.31.0"
7879
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
7980
trybuild = "1.0.99"
8081
url = { version = "2.5.2", features = ["serde"] }

crates/stackable-operator/CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Added
8+
9+
- The default Kubernetes cluster domain name is now fetched from the kubelet API unless explicitly configured ([#1068], [#1071])
10+
This requires operators to have the RBAC permission to `get` `nodes/proxy` in the apiGroup "", an example RBAC rule could look like:
11+
12+
```yaml
13+
---
14+
apiVersion: rbac.authorization.k8s.io/v1
15+
kind: ClusterRole
16+
metadata:
17+
name: operator-cluster-role
18+
rules:
19+
- apiGroups: [""]
20+
resources: [nodes/proxy]
21+
verbs: [get]
22+
```
23+
24+
In addition, they must be provided the environment variable `KUBERNETES_NODE_NAME` like this:
25+
26+
```yaml
27+
env:
28+
- name: KUBERNETES_NODE_NAME
29+
valueFrom:
30+
fieldRef:
31+
fieldPath: spec.nodeName
32+
```
33+
734
### Changed
835

936
- Update `kube` to `1.1.0` ([#1049]).
@@ -23,6 +50,8 @@ All notable changes to this project will be documented in this file.
2350
[#1058]: https://github.com/stackabletech/operator-rs/pull/1058
2451
[#1060]: https://github.com/stackabletech/operator-rs/pull/1060
2552
[#1064]: https://github.com/stackabletech/operator-rs/pull/1064
53+
[#1068]: https://github.com/stackabletech/operator-rs/pull/1068
54+
[#1071]: https://github.com/stackabletech/operator-rs/pull/1071
2655

2756
## [0.93.2] - 2025-05-26
2857

@@ -148,7 +177,7 @@ All notable changes to this project will be documented in this file.
148177
### Added
149178

150179
- Add Deployments to `ClusterResource`s ([#992]).
151-
- Add `DeploymentConditionBuilder` ([#993]).
180+
- Add `DeploymentConditionBuilder` ([#993]).
152181

153182
### Changed
154183

@@ -369,7 +398,7 @@ All notable changes to this project will be documented in this file.
369398
### Fixed
370399

371400
- BREAKING: `KeyValuePairs::insert` (as well as `Labels::`/`Annotations::` via it) now overwrites
372-
the old value if the key already exists. Previously, `iter()` would return *both* values in
401+
the old value if the key already exists. Previously, `iter()` would return _both_ values in
373402
lexicographical order (causing further conversions like `Into<BTreeMap>` to prefer the maximum
374403
value) ([#888]).
375404

@@ -634,7 +663,7 @@ All notable changes to this project will be documented in this file.
634663

635664
### Changed
636665

637-
- Implement `PartialEq` for most *Snafu* Error enums ([#757]).
666+
- Implement `PartialEq` for most _Snafu_ Error enums ([#757]).
638667
- Update Rust to 1.77 ([#759])
639668

640669
### Fixed
@@ -1385,7 +1414,7 @@ This is a rerelease of 0.25.1 which some last-minute incompatible API changes to
13851414
### Changed
13861415

13871416
- Objects are now streamed rather than polled when waiting for them to be deleted ([#452]).
1388-
- serde\_yaml 0.8.26 -> 0.9.9 ([#450])
1417+
- serde_yaml 0.8.26 -> 0.9.9 ([#450])
13891418

13901419
[#450]: https://github.com/stackabletech/operator-rs/pull/450
13911420
[#452]: https://github.com/stackabletech/operator-rs/pull/452

crates/stackable-operator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dockerfile-parser.workspace = true
2828
either.workspace = true
2929
educe.workspace = true
3030
futures.workspace = true
31+
http.workspace = true
3132
indexmap.workspace = true
3233
json-patch.workspace = true
3334
k8s-openapi.workspace = true

crates/stackable-operator/src/cli.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ pub enum Command<Run: Args = ProductOperatorRun> {
165165
/// ```rust
166166
/// # use stackable_operator::cli::{Command, ProductOperatorRun, ProductConfigPath};
167167
/// use clap::Parser;
168-
/// use stackable_operator::namespace::WatchNamespace;
168+
/// use stackable_operator::{namespace::WatchNamespace, utils::cluster_info::KubernetesClusterInfoOpts};
169169
/// use stackable_telemetry::tracing::TelemetryOptions;
170170
///
171171
/// #[derive(clap::Parser, Debug, PartialEq, Eq)]
@@ -176,14 +176,17 @@ pub enum Command<Run: Args = ProductOperatorRun> {
176176
/// common: ProductOperatorRun,
177177
/// }
178178
///
179-
/// let opts = Command::<Run>::parse_from(["foobar-operator", "run", "--name", "foo", "--product-config", "bar", "--watch-namespace", "foobar"]);
179+
/// let opts = Command::<Run>::parse_from(["foobar-operator", "run", "--name", "foo", "--product-config", "bar", "--watch-namespace", "foobar", "--kubernetes-node-name", "baz"]);
180180
/// assert_eq!(opts, Command::Run(Run {
181181
/// name: "foo".to_string(),
182182
/// common: ProductOperatorRun {
183183
/// product_config: ProductConfigPath::from("bar".as_ref()),
184184
/// watch_namespace: WatchNamespace::One("foobar".to_string()),
185185
/// telemetry_arguments: TelemetryOptions::default(),
186-
/// cluster_info_opts: Default::default(),
186+
/// cluster_info_opts: KubernetesClusterInfoOpts {
187+
/// kubernetes_cluster_domain: None,
188+
/// kubernetes_node_name: "baz".to_string(),
189+
/// },
187190
/// },
188191
/// }));
189192
/// ```
@@ -388,38 +391,61 @@ mod tests {
388391
"bar",
389392
"--watch-namespace",
390393
"foo",
394+
"--kubernetes-node-name",
395+
"baz",
391396
]);
392397
assert_eq!(
393398
opts,
394399
ProductOperatorRun {
395400
product_config: ProductConfigPath::from("bar".as_ref()),
396401
watch_namespace: WatchNamespace::One("foo".to_string()),
397-
cluster_info_opts: Default::default(),
402+
cluster_info_opts: KubernetesClusterInfoOpts {
403+
kubernetes_cluster_domain: None,
404+
kubernetes_node_name: "baz".to_string()
405+
},
398406
telemetry_arguments: Default::default(),
399407
}
400408
);
401409

402410
// no cli / no env
403-
let opts = ProductOperatorRun::parse_from(["run", "--product-config", "bar"]);
411+
let opts = ProductOperatorRun::parse_from([
412+
"run",
413+
"--product-config",
414+
"bar",
415+
"--kubernetes-node-name",
416+
"baz",
417+
]);
404418
assert_eq!(
405419
opts,
406420
ProductOperatorRun {
407421
product_config: ProductConfigPath::from("bar".as_ref()),
408422
watch_namespace: WatchNamespace::All,
409-
cluster_info_opts: Default::default(),
423+
cluster_info_opts: KubernetesClusterInfoOpts {
424+
kubernetes_cluster_domain: None,
425+
kubernetes_node_name: "baz".to_string()
426+
},
410427
telemetry_arguments: Default::default(),
411428
}
412429
);
413430

414431
// env with namespace
415432
unsafe { env::set_var(WATCH_NAMESPACE, "foo") };
416-
let opts = ProductOperatorRun::parse_from(["run", "--product-config", "bar"]);
433+
let opts = ProductOperatorRun::parse_from([
434+
"run",
435+
"--product-config",
436+
"bar",
437+
"--kubernetes-node-name",
438+
"baz",
439+
]);
417440
assert_eq!(
418441
opts,
419442
ProductOperatorRun {
420443
product_config: ProductConfigPath::from("bar".as_ref()),
421444
watch_namespace: WatchNamespace::One("foo".to_string()),
422-
cluster_info_opts: Default::default(),
445+
cluster_info_opts: KubernetesClusterInfoOpts {
446+
kubernetes_cluster_domain: None,
447+
kubernetes_node_name: "baz".to_string()
448+
},
423449
telemetry_arguments: Default::default(),
424450
}
425451
);

crates/stackable-operator/src/client.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ pub enum Error {
8484

8585
#[snafu(display("unable to create kubernetes client"))]
8686
CreateKubeClient { source: kube::Error },
87+
88+
#[snafu(display("unable to fetch cluster information from kubelet"))]
89+
NewKubeletClusterInfo {
90+
source: crate::utils::cluster_info::Error,
91+
},
8792
}
8893

8994
/// This `Client` can be used to access Kubernetes.
@@ -518,15 +523,19 @@ impl Client {
518523
///
519524
/// ```no_run
520525
/// use std::time::Duration;
526+
/// use clap::Parser;
521527
/// use tokio::time::error::Elapsed;
522528
/// use kube::runtime::watcher;
523529
/// use k8s_openapi::api::core::v1::Pod;
524-
/// use stackable_operator::client::{Client, initialize_operator};
530+
/// use stackable_operator::{
531+
/// client::{Client, initialize_operator},
532+
/// utils::cluster_info::KubernetesClusterInfoOpts,
533+
/// };
525534
///
526535
/// #[tokio::main]
527536
/// async fn main() {
528-
///
529-
/// let client = initialize_operator(None, &Default::default())
537+
/// let cluster_info_opts = KubernetesClusterInfoOpts::parse();
538+
/// let client = initialize_operator(None, &cluster_info_opts)
530539
/// .await
531540
/// .expect("Unable to construct client.");
532541
/// let watcher_config: watcher::Config =
@@ -651,7 +660,9 @@ pub async fn initialize_operator(
651660
.context(InferKubeConfigSnafu)?;
652661
let default_namespace = kubeconfig.default_namespace.clone();
653662
let client = kube::Client::try_from(kubeconfig).context(CreateKubeClientSnafu)?;
654-
let cluster_info = KubernetesClusterInfo::new(cluster_info_opts);
663+
let cluster_info = KubernetesClusterInfo::new(&client, cluster_info_opts)
664+
.await
665+
.context(NewKubeletClusterInfoSnafu)?;
655666

656667
Ok(Client::new(
657668
client,
@@ -676,10 +687,26 @@ mod tests {
676687
};
677688
use tokio::time::error::Elapsed;
678689

690+
use crate::utils::cluster_info::KubernetesClusterInfoOpts;
691+
692+
async fn test_cluster_info_opts() -> KubernetesClusterInfoOpts {
693+
KubernetesClusterInfoOpts {
694+
// We have to hard-code a made-up cluster domain,
695+
// since kubernetes_node_name (probably) won't be a valid Node that we can query.
696+
kubernetes_cluster_domain: Some(
697+
"fake-cluster.local"
698+
.parse()
699+
.expect("hard-coded cluster domain must be valid"),
700+
),
701+
// Tests aren't running in a kubelet, so make up a name of one.
702+
kubernetes_node_name: "fake-node-name".to_string(),
703+
}
704+
}
705+
679706
#[tokio::test]
680707
#[ignore = "Tests depending on Kubernetes are not ran by default"]
681708
async fn k8s_test_wait_created() {
682-
let client = super::initialize_operator(None, &Default::default())
709+
let client = super::initialize_operator(None, &test_cluster_info_opts().await)
683710
.await
684711
.expect("KUBECONFIG variable must be configured.");
685712

@@ -757,7 +784,7 @@ mod tests {
757784
#[tokio::test]
758785
#[ignore = "Tests depending on Kubernetes are not ran by default"]
759786
async fn k8s_test_wait_created_timeout() {
760-
let client = super::initialize_operator(None, &Default::default())
787+
let client = super::initialize_operator(None, &test_cluster_info_opts().await)
761788
.await
762789
.expect("KUBECONFIG variable must be configured.");
763790

@@ -777,7 +804,7 @@ mod tests {
777804
#[tokio::test]
778805
#[ignore = "Tests depending on Kubernetes are not ran by default"]
779806
async fn k8s_test_list_with_label_selector() {
780-
let client = super::initialize_operator(None, &Default::default())
807+
let client = super::initialize_operator(None, &test_cluster_info_opts().await)
781808
.await
782809
.expect("KUBECONFIG variable must be configured.");
783810

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,62 @@
1-
use std::str::FromStr;
1+
use kube::Client;
2+
use snafu::{ResultExt, Snafu};
23

3-
use crate::commons::networking::DomainName;
4+
use crate::{commons::networking::DomainName, utils::kubelet};
45

5-
const KUBERNETES_CLUSTER_DOMAIN_DEFAULT: &str = "cluster.local";
6+
#[derive(Debug, Snafu)]
7+
pub enum Error {
8+
#[snafu(display("unable to fetch kubelet config"))]
9+
KubeletConfig { source: kubelet::Error },
10+
}
611

7-
/// Some information that we know about the Kubernetes cluster.
812
#[derive(Debug, Clone)]
913
pub struct KubernetesClusterInfo {
1014
/// The Kubernetes cluster domain, typically `cluster.local`.
1115
pub cluster_domain: DomainName,
1216
}
1317

14-
#[derive(clap::Parser, Debug, Default, PartialEq, Eq)]
18+
#[derive(clap::Parser, Debug, PartialEq, Eq)]
1519
pub struct KubernetesClusterInfoOpts {
1620
/// Kubernetes cluster domain, usually this is `cluster.local`.
17-
// We are not using a default value here, as operators will probably do an more advanced
18-
// auto-detection of the cluster domain in case it is not specified in the future.
21+
// We are not using a default value here, as we query the cluster if it is not specified.
1922
#[arg(long, env)]
2023
pub kubernetes_cluster_domain: Option<DomainName>,
24+
25+
/// Name of the Kubernetes Node that the operator is running on.
26+
#[arg(long, env)]
27+
pub kubernetes_node_name: String,
2128
}
2229

2330
impl KubernetesClusterInfo {
24-
pub fn new(cluster_info_opts: &KubernetesClusterInfoOpts) -> Self {
25-
let cluster_domain = match &cluster_info_opts.kubernetes_cluster_domain {
26-
Some(cluster_domain) => {
31+
pub async fn new(
32+
client: &Client,
33+
cluster_info_opts: &KubernetesClusterInfoOpts,
34+
) -> Result<Self, Error> {
35+
let cluster_domain = match cluster_info_opts {
36+
KubernetesClusterInfoOpts {
37+
kubernetes_cluster_domain: Some(cluster_domain),
38+
..
39+
} => {
2740
tracing::info!(%cluster_domain, "Using configured Kubernetes cluster domain");
2841

2942
cluster_domain.clone()
3043
}
31-
None => {
32-
// TODO(sbernauer): Do some sort of advanced auto-detection, see https://github.com/stackabletech/issues/issues/436.
33-
// There have been attempts of parsing the `/etc/resolv.conf`, but they have been
34-
// reverted. Please read on the linked issue for details.
35-
let cluster_domain = DomainName::from_str(KUBERNETES_CLUSTER_DOMAIN_DEFAULT)
36-
.expect("KUBERNETES_CLUSTER_DOMAIN_DEFAULT constant must a valid domain");
37-
tracing::info!(%cluster_domain, "Defaulting Kubernetes cluster domain as it has not been configured");
44+
KubernetesClusterInfoOpts {
45+
kubernetes_node_name: node_name,
46+
..
47+
} => {
48+
tracing::info!(%node_name, "Fetching Kubernetes cluster domain from the local kubelet");
49+
let kubelet_config = kubelet::KubeletConfig::fetch(client, node_name)
50+
.await
51+
.context(KubeletConfigSnafu)?;
52+
53+
let cluster_domain = kubelet_config.cluster_domain;
54+
tracing::info!(%cluster_domain, "Using Kubernetes cluster domain from the kubelet config");
3855

3956
cluster_domain
4057
}
4158
};
4259

43-
Self { cluster_domain }
60+
Ok(Self { cluster_domain })
4461
}
4562
}

0 commit comments

Comments
 (0)