Skip to content

Commit a548bc5

Browse files
sbernauerTechassi
andauthored
feat!: Add working conversion webhook with cert rotation (#1066)
* WIP * Actually rotate certs * clippy * doctests * HashMap -> Vec * Set correct CA lifetime * Result type * fix rustdocs * Add some docs * Update rustdoc * Handle cert rottion loop falures * changelog * link to decision * fix rustdocs * Rework CLI structs and names * test: Remove CLI parsing from env var test * Move subject_alterative_dns_names into Options * changelog * Remove mpsc in tests leftover * docs: Use result of WebhookServer::new * fmt * Update crates/stackable-webhook/src/tls/mod.rs Co-authored-by: Techassi <[email protected]> * fix suggestion * capture in variable * Move route into variable * docs: Mention background cert rotation * docs: Document TlsServer::new * docs: Hint on downward API env var * Rewrite certificate rotation logic Co-authored-by: Techassi <[email protected]> * Avoid unwrap * Make CertResolver pub, so docs pass * Add missing await * mount -> project * Update crates/stackable-webhook/src/tls/mod.rs Co-authored-by: Techassi <[email protected]> * changelog * Add ConversionWebhookOptions struct * PascalCase error variants * changelog * changelog * fix rustdocs * Update crates/stackable-operator/CHANGELOG.md Co-authored-by: Techassi <[email protected]> * ReceiverCertificateFromChannel -> ReceiveCertificateFromChannel * Remove double re-export * fixup * Update crates/stackable-webhook/src/tls/cert_resolver.rs Co-authored-by: Techassi <[email protected]> * Use CryptoProvider::get_default * Update crates/stackable-webhook/src/tls/cert_resolver.rs Co-authored-by: Techassi <[email protected]> * changelog * Remove CLI parsing tests * clippy * Move stuff into send_certificate_to_channel * OptionsBuilder -> WebhookOptionsBuilder * fix typo with read and write * Refactor cert resolver * chore: Dont use OperatorEnvironmentOptions * fix outdated docs * fix doc test * fix clippy lints * Improve rust docs * Further improve docs * Add CONVERSION_WEBHOOK_HTTPS_PORT * update docs * suggest 0.0.0.0 in docs * markdown linter * Refactor stuff into generate_new_certificatei Co-authored-by: Techassi <[email protected]> * chore: Increase TLS certificate lifetime to 24 hours --------- Co-authored-by: Techassi <[email protected]>
1 parent b5f9a01 commit a548bc5

File tree

19 files changed

+970
-594
lines changed

19 files changed

+970
-594
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ repository = "https://github.com/stackabletech/operator-rs"
1111
[workspace.dependencies]
1212
product-config = { git = "https://github.com/stackabletech/product-config.git", tag = "0.7.0" }
1313

14+
arc-swap = "1.7"
1415
axum = { version = "0.8.1", features = ["http2"] }
1516
chrono = { version = "0.4.38", default-features = false }
1617
clap = { version = "4.5.17", features = ["derive", "cargo", "env"] }

crates/stackable-certs/src/ca/consts.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use stackable_operator::time::Duration;
22

3-
/// The default CA validity time span of one hour (3600 seconds).
3+
/// The default CA validity time span
44
pub const DEFAULT_CA_VALIDITY: Duration = Duration::from_hours_unchecked(1);
55

66
/// The root CA subject name containing only the common name.

crates/stackable-certs/src/ca/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub enum Error {
3838
#[snafu(display("failed to generate RSA signing key"))]
3939
GenerateRsaSigningKey { source: rsa::Error },
4040

41-
#[snafu(display("failed to generate ECDSA signign key"))]
41+
#[snafu(display("failed to generate ECDSA signing key"))]
4242
GenerateEcdsaSigningKey { source: ecdsa::Error },
4343

4444
#[snafu(display("failed to parse {subject:?} as subject"))]

crates/stackable-operator/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ All notable changes to this project will be documented in this file.
77
### Added
88

99
- Add `ProbeBuilder` to build Kubernetes container probes ([#1078]).
10+
- BREAKING: Add two new required CLI arguments: `--operator-namespace` and `--operator-service-name`.
11+
These two values are used to construct the service name in the CRD conversion webhook ([#1066]).
1012

1113
### Changed
1214

1315
- BREAKING: The `ResolvedProductImage` field `app_version_label` was renamed to `app_version_label_value` to match changes to its type ([#1076]).
16+
- BREAKING: Rename two fields of the `ProductOperatorRun` struct for consistency and clarity ([#1066]):
17+
- `telemetry_arguments` -> `telemetry`
18+
- `cluster_info_opts` -> `cluster_info`
1419

1520
### Fixed
1621

1722
- BREAKING: Fix bug where `ResolvedProductImage::app_version_label` could not be used as a label value because it can contain invalid characters.
1823
This is the case when referencing custom images via a `@sha256:...` hash. As such, the `product_image_selection::resolve` function is now fallible ([#1076]).
1924

25+
[#1066]: https://github.com/stackabletech/operator-rs/pull/1066
2026
[#1076]: https://github.com/stackabletech/operator-rs/pull/1076
2127
[#1078]: https://github.com/stackabletech/operator-rs/pull/1078
2228

crates/stackable-operator/src/cli.rs

Lines changed: 48 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ use product_config::ProductConfigManager;
116116
use snafu::{ResultExt, Snafu};
117117
use stackable_telemetry::tracing::TelemetryOptions;
118118

119-
use crate::{namespace::WatchNamespace, utils::cluster_info::KubernetesClusterInfoOpts};
119+
use crate::{namespace::WatchNamespace, utils::cluster_info::KubernetesClusterInfoOptions};
120120

121121
pub const AUTHOR: &str = "Stackable GmbH - [email protected]";
122122

@@ -163,10 +163,10 @@ pub enum Command<Run: Args = ProductOperatorRun> {
163163
/// Can be embedded into an extended argument set:
164164
///
165165
/// ```rust
166-
/// # use stackable_operator::cli::{Command, ProductOperatorRun, ProductConfigPath};
166+
/// # use stackable_operator::cli::{Command, OperatorEnvironmentOptions, ProductOperatorRun, ProductConfigPath};
167+
/// # use stackable_operator::{namespace::WatchNamespace, utils::cluster_info::KubernetesClusterInfoOptions};
168+
/// # use stackable_telemetry::tracing::TelemetryOptions;
167169
/// use clap::Parser;
168-
/// use stackable_operator::{namespace::WatchNamespace, utils::cluster_info::KubernetesClusterInfoOpts};
169-
/// use stackable_telemetry::tracing::TelemetryOptions;
170170
///
171171
/// #[derive(clap::Parser, Debug, PartialEq, Eq)]
172172
/// struct Run {
@@ -176,17 +176,36 @@ 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", "--kubernetes-node-name", "baz"]);
179+
/// let opts = Command::<Run>::parse_from([
180+
/// "foobar-operator",
181+
/// "run",
182+
/// "--name",
183+
/// "foo",
184+
/// "--product-config",
185+
/// "bar",
186+
/// "--watch-namespace",
187+
/// "foobar",
188+
/// "--operator-namespace",
189+
/// "stackable-operators",
190+
/// "--operator-service-name",
191+
/// "foo-operator",
192+
/// "--kubernetes-node-name",
193+
/// "baz",
194+
/// ]);
180195
/// assert_eq!(opts, Command::Run(Run {
181196
/// name: "foo".to_string(),
182197
/// common: ProductOperatorRun {
183198
/// product_config: ProductConfigPath::from("bar".as_ref()),
184199
/// watch_namespace: WatchNamespace::One("foobar".to_string()),
185-
/// telemetry_arguments: TelemetryOptions::default(),
186-
/// cluster_info_opts: KubernetesClusterInfoOpts {
200+
/// telemetry: TelemetryOptions::default(),
201+
/// cluster_info: KubernetesClusterInfoOptions {
187202
/// kubernetes_cluster_domain: None,
188203
/// kubernetes_node_name: "baz".to_string(),
189204
/// },
205+
/// operator_environment: OperatorEnvironmentOptions {
206+
/// operator_namespace: "stackable-operators".to_string(),
207+
/// operator_service_name: "foo-operator".to_string(),
208+
/// },
190209
/// },
191210
/// }));
192211
/// ```
@@ -220,10 +239,13 @@ pub struct ProductOperatorRun {
220239
pub watch_namespace: WatchNamespace,
221240

222241
#[command(flatten)]
223-
pub telemetry_arguments: TelemetryOptions,
242+
pub operator_environment: OperatorEnvironmentOptions,
243+
244+
#[command(flatten)]
245+
pub telemetry: TelemetryOptions,
224246

225247
#[command(flatten)]
226-
pub cluster_info_opts: KubernetesClusterInfoOpts,
248+
pub cluster_info: KubernetesClusterInfoOptions,
227249
}
228250

229251
/// A path to a [`ProductConfigManager`] spec file
@@ -281,11 +303,26 @@ impl ProductConfigPath {
281303
}
282304
}
283305

306+
#[derive(clap::Parser, Debug, PartialEq, Eq)]
307+
pub struct OperatorEnvironmentOptions {
308+
/// The namespace the operator is running in, usually `stackable-operators`.
309+
///
310+
/// Note that when running the operator on Kubernetes we recommend to use the
311+
/// [downward API](https://kubernetes.io/docs/concepts/workloads/pods/downward-api/)
312+
/// to let Kubernetes project the namespace as the `OPERATOR_NAMESPACE` env variable.
313+
#[arg(long, env)]
314+
pub operator_namespace: String,
315+
316+
/// The name of the service the operator is reachable at, usually
317+
/// something like `<product>-operator`.
318+
#[arg(long, env)]
319+
pub operator_service_name: String,
320+
}
321+
284322
#[cfg(test)]
285323
mod tests {
286-
use std::{env, fs::File};
324+
use std::fs::File;
287325

288-
use clap::Parser;
289326
use rstest::*;
290327
use tempfile::tempdir;
291328

@@ -294,7 +331,6 @@ mod tests {
294331
const USER_PROVIDED_PATH: &str = "user_provided_path_properties.yaml";
295332
const DEPLOY_FILE_PATH: &str = "deploy_config_spec_properties.yaml";
296333
const DEFAULT_FILE_PATH: &str = "default_file_path_properties.yaml";
297-
const WATCH_NAMESPACE: &str = "WATCH_NAMESPACE";
298334

299335
#[test]
300336
fn verify_cli() {
@@ -378,76 +414,4 @@ mod tests {
378414
panic!("must return RequiredFileMissing when file was not found")
379415
}
380416
}
381-
382-
#[test]
383-
fn product_operator_run_watch_namespace() {
384-
// clean env var to not interfere if already set
385-
unsafe { env::remove_var(WATCH_NAMESPACE) };
386-
387-
// cli with namespace
388-
let opts = ProductOperatorRun::parse_from([
389-
"run",
390-
"--product-config",
391-
"bar",
392-
"--watch-namespace",
393-
"foo",
394-
"--kubernetes-node-name",
395-
"baz",
396-
]);
397-
assert_eq!(
398-
opts,
399-
ProductOperatorRun {
400-
product_config: ProductConfigPath::from("bar".as_ref()),
401-
watch_namespace: WatchNamespace::One("foo".to_string()),
402-
cluster_info_opts: KubernetesClusterInfoOpts {
403-
kubernetes_cluster_domain: None,
404-
kubernetes_node_name: "baz".to_string()
405-
},
406-
telemetry_arguments: Default::default(),
407-
}
408-
);
409-
410-
// no cli / no env
411-
let opts = ProductOperatorRun::parse_from([
412-
"run",
413-
"--product-config",
414-
"bar",
415-
"--kubernetes-node-name",
416-
"baz",
417-
]);
418-
assert_eq!(
419-
opts,
420-
ProductOperatorRun {
421-
product_config: ProductConfigPath::from("bar".as_ref()),
422-
watch_namespace: WatchNamespace::All,
423-
cluster_info_opts: KubernetesClusterInfoOpts {
424-
kubernetes_cluster_domain: None,
425-
kubernetes_node_name: "baz".to_string()
426-
},
427-
telemetry_arguments: Default::default(),
428-
}
429-
);
430-
431-
// env with namespace
432-
unsafe { env::set_var(WATCH_NAMESPACE, "foo") };
433-
let opts = ProductOperatorRun::parse_from([
434-
"run",
435-
"--product-config",
436-
"bar",
437-
"--kubernetes-node-name",
438-
"baz",
439-
]);
440-
assert_eq!(
441-
opts,
442-
ProductOperatorRun {
443-
product_config: ProductConfigPath::from("bar".as_ref()),
444-
watch_namespace: WatchNamespace::One("foo".to_string()),
445-
cluster_info_opts: KubernetesClusterInfoOpts {
446-
kubernetes_cluster_domain: None,
447-
kubernetes_node_name: "baz".to_string()
448-
},
449-
telemetry_arguments: Default::default(),
450-
}
451-
);
452-
}
453417
}

crates/stackable-operator/src/client.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use tracing::trace;
2121

2222
use crate::{
2323
kvp::LabelSelectorExt,
24-
utils::cluster_info::{KubernetesClusterInfo, KubernetesClusterInfoOpts},
24+
utils::cluster_info::{KubernetesClusterInfo, KubernetesClusterInfoOptions},
2525
};
2626

2727
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -529,13 +529,13 @@ impl Client {
529529
/// use k8s_openapi::api::core::v1::Pod;
530530
/// use stackable_operator::{
531531
/// client::{Client, initialize_operator},
532-
/// utils::cluster_info::KubernetesClusterInfoOpts,
532+
/// utils::cluster_info::KubernetesClusterInfoOptions,
533533
/// };
534534
///
535535
/// #[tokio::main]
536536
/// async fn main() {
537-
/// let cluster_info_opts = KubernetesClusterInfoOpts::parse();
538-
/// let client = initialize_operator(None, &cluster_info_opts)
537+
/// let cluster_info_options = KubernetesClusterInfoOptions::parse();
538+
/// let client = initialize_operator(None, &cluster_info_options)
539539
/// .await
540540
/// .expect("Unable to construct client.");
541541
/// let watcher_config: watcher::Config =
@@ -652,7 +652,7 @@ where
652652

653653
pub async fn initialize_operator(
654654
field_manager: Option<String>,
655-
cluster_info_opts: &KubernetesClusterInfoOpts,
655+
cluster_info_opts: &KubernetesClusterInfoOptions,
656656
) -> Result<Client> {
657657
let kubeconfig: Config = kube::Config::infer()
658658
.await
@@ -687,10 +687,10 @@ mod tests {
687687
};
688688
use tokio::time::error::Elapsed;
689689

690-
use crate::utils::cluster_info::KubernetesClusterInfoOpts;
690+
use crate::utils::cluster_info::KubernetesClusterInfoOptions;
691691

692-
async fn test_cluster_info_opts() -> KubernetesClusterInfoOpts {
693-
KubernetesClusterInfoOpts {
692+
async fn test_cluster_info_opts() -> KubernetesClusterInfoOptions {
693+
KubernetesClusterInfoOptions {
694694
// We have to hard-code a made-up cluster domain,
695695
// since kubernetes_node_name (probably) won't be a valid Node that we can query.
696696
kubernetes_cluster_domain: Some(

crates/stackable-operator/src/utils/cluster_info.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,36 @@ pub struct KubernetesClusterInfo {
1616
}
1717

1818
#[derive(clap::Parser, Debug, PartialEq, Eq)]
19-
pub struct KubernetesClusterInfoOpts {
19+
pub struct KubernetesClusterInfoOptions {
2020
/// Kubernetes cluster domain, usually this is `cluster.local`.
2121
// We are not using a default value here, as we query the cluster if it is not specified.
2222
#[arg(long, env)]
2323
pub kubernetes_cluster_domain: Option<DomainName>,
2424

2525
/// Name of the Kubernetes Node that the operator is running on.
26+
///
27+
/// Note that when running the operator on Kubernetes we recommend to use the
28+
/// [downward API](https://kubernetes.io/docs/concepts/workloads/pods/downward-api/)
29+
/// to let Kubernetes project the namespace as the `KUBERNETES_NODE_NAME` env variable.
2630
#[arg(long, env)]
2731
pub kubernetes_node_name: String,
2832
}
2933

3034
impl KubernetesClusterInfo {
3135
pub async fn new(
3236
client: &Client,
33-
cluster_info_opts: &KubernetesClusterInfoOpts,
37+
cluster_info_opts: &KubernetesClusterInfoOptions,
3438
) -> Result<Self, Error> {
3539
let cluster_domain = match cluster_info_opts {
36-
KubernetesClusterInfoOpts {
40+
KubernetesClusterInfoOptions {
3741
kubernetes_cluster_domain: Some(cluster_domain),
3842
..
3943
} => {
4044
tracing::info!(%cluster_domain, "Using configured Kubernetes cluster domain");
4145

4246
cluster_domain.clone()
4347
}
44-
KubernetesClusterInfoOpts {
48+
KubernetesClusterInfoOptions {
4549
kubernetes_node_name: node_name,
4650
..
4751
} => {

crates/stackable-telemetry/src/instrumentation/axum/mod.rs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,6 @@ const OTEL_TRACE_ID_TO: &str = "opentelemetry.trace_id.to";
7373
/// # let _: Router = router;
7474
/// ```
7575
///
76-
/// ### Example with Webhook
77-
///
78-
/// The usage is even simpler when combined with the `stackable_webhook` crate.
79-
/// The webhook server has built-in support to automatically emit HTTP spans on
80-
/// every incoming request.
81-
///
82-
/// ```
83-
/// use stackable_webhook::{WebhookServer, Options};
84-
/// use axum::Router;
85-
///
86-
/// let router = Router::new();
87-
/// let server = WebhookServer::new(router, Options::default());
88-
///
89-
/// # let _: WebhookServer = server;
90-
/// ```
91-
///
9276
/// This layer is implemented based on [this][1] official Tower guide.
9377
///
9478
/// [1]: https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md

0 commit comments

Comments
 (0)