Skip to content

Commit 0c52f5f

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/conversion-webhook
2 parents 941ee6b + 7328943 commit 0c52f5f

File tree

9 files changed

+432
-337
lines changed

9 files changed

+432
-337
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
@@ -26,6 +26,7 @@ educe = { version = "0.6.0", default-features = false, features = ["Clone", "De
2626
either = "1.13.0"
2727
futures = "0.3.30"
2828
futures-util = "0.3.30"
29+
http = "1.3.1"
2930
indexmap = "2.5.0"
3031
indoc = "2.0.6"
3132
insta = { version= "1.40", features = ["glob"] }
@@ -37,11 +38,11 @@ k8s-openapi = { version = "0.25.0", default-features = false, features = ["schem
3738
# 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
3839
# We use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev"
3940
kube = { version = "1.1.0", default-features = false, features = ["client", "jsonpatch", "runtime", "derive", "rustls-tls", "ring"] }
40-
opentelemetry = "0.29.1"
41-
opentelemetry_sdk = { version = "0.29.0", features = ["rt-tokio"] }
42-
opentelemetry-appender-tracing = "0.29.1"
43-
opentelemetry-otlp = "0.29.0"
44-
opentelemetry-semantic-conventions = "0.29.0"
41+
opentelemetry = "0.30.0"
42+
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
43+
opentelemetry-appender-tracing = "0.30.1"
44+
opentelemetry-otlp = "0.30.0"
45+
opentelemetry-semantic-conventions = "0.30.0"
4546
p256 = { version = "0.13.2", features = ["ecdsa"] }
4647
paste = "1.0.15"
4748
pin-project = "1.1.5"
@@ -75,7 +76,7 @@ tower = { version = "0.5.1", features = ["util"] }
7576
tower-http = { version = "0.6.1", features = ["trace"] }
7677
tracing = "0.1.40"
7778
tracing-appender = "0.2.3"
78-
tracing-opentelemetry = "0.30.0"
79+
tracing-opentelemetry = "0.31.0"
7980
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
8081
trybuild = "1.0.99"
8182
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]).
@@ -26,6 +53,8 @@ All notable changes to this project will be documented in this file.
2653
[#1060]: https://github.com/stackabletech/operator-rs/pull/1060
2754
[#1064]: https://github.com/stackabletech/operator-rs/pull/1064
2855
[#1066]: https://github.com/stackabletech/operator-rs/pull/1066
56+
[#1068]: https://github.com/stackabletech/operator-rs/pull/1068
57+
[#1071]: https://github.com/stackabletech/operator-rs/pull/1071
2958

3059
## [0.93.2] - 2025-05-26
3160

@@ -151,7 +180,7 @@ All notable changes to this project will be documented in this file.
151180
### Added
152181

153182
- Add Deployments to `ClusterResource`s ([#992]).
154-
- Add `DeploymentConditionBuilder` ([#993]).
183+
- Add `DeploymentConditionBuilder` ([#993]).
155184

156185
### Changed
157186

@@ -372,7 +401,7 @@ All notable changes to this project will be documented in this file.
372401
### Fixed
373402

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

@@ -637,7 +666,7 @@ All notable changes to this project will be documented in this file.
637666

638667
### Changed
639668

640-
- Implement `PartialEq` for most *Snafu* Error enums ([#757]).
669+
- Implement `PartialEq` for most _Snafu_ Error enums ([#757]).
641670
- Update Rust to 1.77 ([#759])
642671

643672
### Fixed
@@ -1388,7 +1417,7 @@ This is a rerelease of 0.25.1 which some last-minute incompatible API changes to
13881417
### Changed
13891418

13901419
- Objects are now streamed rather than polled when waiting for them to be deleted ([#452]).
1391-
- serde\_yaml 0.8.26 -> 0.9.9 ([#450])
1420+
- serde_yaml 0.8.26 -> 0.9.9 ([#450])
13921421

13931422
[#450]: https://github.com/stackabletech/operator-rs/pull/450
13941423
[#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: 25 additions & 5 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, OperatorEnvironmentOpts, 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)]
@@ -189,14 +189,19 @@ pub enum Command<Run: Args = ProductOperatorRun> {
189189
/// "stackable-operators",
190190
/// "--operator-service-name",
191191
/// "foo-operator",
192+
/// "--kubernetes-node-name",
193+
/// "baz",
192194
/// ]);
193195
/// assert_eq!(opts, Command::Run(Run {
194196
/// name: "foo".to_string(),
195197
/// common: ProductOperatorRun {
196198
/// product_config: ProductConfigPath::from("bar".as_ref()),
197199
/// watch_namespace: WatchNamespace::One("foobar".to_string()),
198200
/// telemetry_arguments: TelemetryOptions::default(),
199-
/// cluster_info_opts: Default::default(),
201+
/// cluster_info_opts: KubernetesClusterInfoOpts {
202+
/// kubernetes_cluster_domain: None,
203+
/// kubernetes_node_name: "baz".to_string(),
204+
/// },
200205
/// operator_environment: OperatorEnvironmentOpts {
201206
/// operator_namespace: "stackable-operators".to_string(),
202207
/// operator_service_name: "foo-operator".to_string(),
@@ -326,6 +331,7 @@ mod tests {
326331
const WATCH_NAMESPACE: &str = "WATCH_NAMESPACE";
327332
const OPERATOR_NAMESPACE: &str = "OPERATOR_NAMESPACE";
328333
const OPERATOR_SERVICE_NAME: &str = "OPERATOR_SERVICE_NAME";
334+
const KUBERNETES_NODE_NAME: &str = "KUBERNETES_NODE_NAME";
329335

330336
#[test]
331337
fn verify_cli() {
@@ -426,13 +432,18 @@ mod tests {
426432
"stackable-operators",
427433
"--operator-service-name",
428434
"foo-operator",
435+
"--kubernetes-node-name",
436+
"baz",
429437
]);
430438
assert_eq!(
431439
opts,
432440
ProductOperatorRun {
433441
product_config: ProductConfigPath::from("bar".as_ref()),
434442
watch_namespace: WatchNamespace::One("foo".to_string()),
435-
cluster_info_opts: Default::default(),
443+
cluster_info_opts: KubernetesClusterInfoOpts {
444+
kubernetes_cluster_domain: None,
445+
kubernetes_node_name: "baz".to_string()
446+
},
436447
telemetry_arguments: Default::default(),
437448
operator_environment: OperatorEnvironmentOpts {
438449
operator_namespace: "stackable-operators".to_string(),
@@ -450,13 +461,18 @@ mod tests {
450461
"stackable-operators",
451462
"--operator-service-name",
452463
"foo-operator",
464+
"--kubernetes-node-name",
465+
"baz",
453466
]);
454467
assert_eq!(
455468
opts,
456469
ProductOperatorRun {
457470
product_config: ProductConfigPath::from("bar".as_ref()),
458471
watch_namespace: WatchNamespace::All,
459-
cluster_info_opts: Default::default(),
472+
cluster_info_opts: KubernetesClusterInfoOpts {
473+
kubernetes_cluster_domain: None,
474+
kubernetes_node_name: "baz".to_string()
475+
},
460476
telemetry_arguments: Default::default(),
461477
operator_environment: OperatorEnvironmentOpts {
462478
operator_namespace: "stackable-operators".to_string(),
@@ -469,14 +485,18 @@ mod tests {
469485
unsafe { env::set_var(WATCH_NAMESPACE, "foo") };
470486
unsafe { env::set_var(OPERATOR_SERVICE_NAME, "foo-operator") };
471487
unsafe { env::set_var(OPERATOR_NAMESPACE, "stackable-operators") };
488+
unsafe { env::set_var(KUBERNETES_NODE_NAME, "baz") };
472489

473490
let opts = ProductOperatorRun::parse_from(["run", "--product-config", "bar"]);
474491
assert_eq!(
475492
opts,
476493
ProductOperatorRun {
477494
product_config: ProductConfigPath::from("bar".as_ref()),
478495
watch_namespace: WatchNamespace::One("foo".to_string()),
479-
cluster_info_opts: Default::default(),
496+
cluster_info_opts: KubernetesClusterInfoOpts {
497+
kubernetes_cluster_domain: None,
498+
kubernetes_node_name: "baz".to_string()
499+
},
480500
telemetry_arguments: Default::default(),
481501
operator_environment: OperatorEnvironmentOpts {
482502
operator_namespace: "stackable-operators".to_string(),

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)