diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c48d21..bbf5ce5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. - `kubeletDir` has been move to `csiNodeDriver.kubeletDir`. - Bump csi-node-driver-registrar to `v2.15.0` ([#642]). - Bump csi-provisioner to `v5.3.0` ([#643]). +- OLM deployer: patch the new Deployment object too and other changes to align with the new operator structure ([#648]). [#597]: https://github.com/stackabletech/secret-operator/pull/597 [#636]: https://github.com/stackabletech/secret-operator/pull/636 @@ -37,6 +38,7 @@ All notable changes to this project will be documented in this file. [#643]: https://github.com/stackabletech/secret-operator/pull/643 [#644]: https://github.com/stackabletech/secret-operator/pull/644 [#645]: https://github.com/stackabletech/secret-operator/pull/645 +[#648]: https://github.com/stackabletech/secret-operator/pull/648 ## [25.7.0] - 2025-07-23 diff --git a/rust/olm-deployer/README.md b/rust/olm-deployer/README.md index 95a27a51..9dfe1262 100644 --- a/rust/olm-deployer/README.md +++ b/rust/olm-deployer/README.md @@ -1,4 +1,25 @@ -# How to test +# Description + +This is an deployment helper for the Operator Lifecycle Manager which is usually present on OpenShift environments. + +It is needed to work around various OLM restrictions. + +What it does: + +- creates Security Context Constraints just for this operator (maybe remove in the future) +- installs the Deployment and DaemonSet objects +- installs the operator webhook service +- installs the CSI driver and storage classes +- assigns it's own deployment as owner of all the namespaced objects to ensure proper cleanup +- patches the environment of all workload containers with any custom values provided in the Subscription object +- patches the resources of all workload containers with any custom values provided in the Subscription object +- patches the tolerations of all workload pods with any custom values provided in the Subscription object + +## Usage + +Users do not need to interact with the OLM deployer directly. + +## How to Test Requirements: diff --git a/rust/olm-deployer/src/data.rs b/rust/olm-deployer/src/data.rs index 3642cb91..0206762b 100644 --- a/rust/olm-deployer/src/data.rs +++ b/rust/olm-deployer/src/data.rs @@ -1,37 +1,12 @@ -use anyhow::{Result, bail}; use stackable_operator::kube::{ResourceExt, api::DynamicObject}; -pub fn data_field_as_mut<'a>( - value: &'a mut serde_json::Value, - pointer: &str, -) -> Result<&'a mut serde_json::Value> { - match value.pointer_mut(pointer) { - Some(field) => Ok(field), - x => bail!("invalid pointer {pointer} for object {x:?}"), - } -} - -pub fn container<'a>( +pub fn containers<'a>( target: &'a mut DynamicObject, - container_name: &str, -) -> anyhow::Result<&'a mut serde_json::Value> { +) -> anyhow::Result<&'a mut Vec> { let tname = target.name_any(); let path = "template/spec/containers".split("/"); match get_or_create(target.data.pointer_mut("/spec").unwrap(), path)? { - serde_json::Value::Array(containers) => { - for c in containers { - if c.is_object() { - if let Some(serde_json::Value::String(name)) = c.get("name") { - if container_name == name { - return Ok(c); - } - } - } else { - anyhow::bail!("container is not a object: {:?}", c); - } - } - anyhow::bail!("container named {container_name} not found"); - } + serde_json::Value::Array(containers) => Ok(containers), _ => anyhow::bail!("no containers found in object {tname}"), } } diff --git a/rust/olm-deployer/src/env/mod.rs b/rust/olm-deployer/src/env/mod.rs index d9c421dc..d05da7d2 100644 --- a/rust/olm-deployer/src/env/mod.rs +++ b/rust/olm-deployer/src/env/mod.rs @@ -6,41 +6,46 @@ use stackable_operator::{ }, }; -use crate::data::container; +use crate::data::containers; /// Copy the environment from the "secret-operator-deployer" container in `source` -/// to the container "secret-operator" in `target`. -/// The `target` must be a DaemonSet object otherwise this is a no-op. +/// to *all* containers in target. +/// The target must be a DaemonSet or Deployment, otherwise this function is a no-op. +/// This function allows OLM Subscription objects to configure the environment +/// of operator containers. pub(super) fn maybe_copy_env( source: &Deployment, target: &mut DynamicObject, target_gvk: &GroupVersionKind, ) -> anyhow::Result<()> { - if target_gvk.kind == "DaemonSet" { + let target_kind_set = ["DaemonSet", "Deployment"]; + if target_kind_set.contains(&target_gvk.kind.as_str()) { if let Some(env) = deployer_env_var(source) { - match container(target, "secret-operator")? { - serde_json::Value::Object(c) => { - let json_env = env - .iter() - .map(|e| serde_json::json!(e)) - .collect::>(); - - match c.get_mut("env") { - Some(env) => match env { - v @ serde_json::Value::Null => { - *v = serde_json::json!(json_env); + for container in containers(target)? { + match container { + serde_json::Value::Object(c) => { + let json_env = env + .iter() + .map(|e| serde_json::json!(e)) + .collect::>(); + + match c.get_mut("env") { + Some(env) => match env { + v @ serde_json::Value::Null => { + *v = serde_json::json!(json_env); + } + serde_json::Value::Array(container_env) => { + container_env.extend_from_slice(&json_env) + } + _ => anyhow::bail!("env is not null or an array"), + }, + None => { + c.insert("env".to_string(), serde_json::json!(json_env)); } - serde_json::Value::Array(container_env) => { - container_env.extend_from_slice(&json_env) - } - _ => anyhow::bail!("env is not null or an array"), - }, - None => { - c.insert("env".to_string(), serde_json::json!(json_env)); } } + _ => anyhow::bail!("no containers found in object {}", target.name_any()), } - _ => anyhow::bail!("no containers found in object {}", target.name_any()), } } } @@ -149,7 +154,9 @@ spec: }, ]); assert_eq!( - container(&mut daemonset, "secret-operator")? + containers(&mut daemonset)? + .first() + .expect("daemonset has no containers") .get("env") .unwrap(), &expected diff --git a/rust/olm-deployer/src/main.rs b/rust/olm-deployer/src/main.rs index 81aa506c..f3afecee 100644 --- a/rust/olm-deployer/src/main.rs +++ b/rust/olm-deployer/src/main.rs @@ -13,7 +13,6 @@ /// mod data; mod env; -mod namespace; mod owner; mod resources; mod tolerations; @@ -23,6 +22,7 @@ use clap::Parser; use stackable_operator::{ cli::Command, client, + commons::networking::DomainName, k8s_openapi::api::{apps::v1::Deployment, rbac::v1::ClusterRole}, kube::{ self, @@ -71,9 +71,6 @@ struct OlmDeployerRun { #[command(flatten)] pub telemetry: TelemetryOptions, - - #[command(flatten)] - pub cluster_info: KubernetesClusterInfoOptions, } #[tokio::main] @@ -85,7 +82,6 @@ async fn main() -> Result<()> { namespace, dir, telemetry, - cluster_info, }) = opts.cmd { // NOTE (@NickLarsenNZ): Before stackable-telemetry was used: @@ -104,7 +100,13 @@ async fn main() -> Result<()> { description = built_info::PKG_DESCRIPTION ); - let client = client::initialize_operator(Some(APP_NAME.to_string()), &cluster_info).await?; + let dummy_cluster_info = KubernetesClusterInfoOptions { + kubernetes_cluster_domain: Some(DomainName::try_from("cluster.local")?), + kubernetes_node_name: "".to_string(), + }; + + let client = + client::initialize_operator(Some(APP_NAME.to_string()), &dummy_cluster_info).await?; let deployment = get_deployment(&csv, &namespace, &client).await?; let cluster_role = get_cluster_role(&csv, &client).await?; @@ -143,7 +145,6 @@ async fn main() -> Result<()> { &deployment, &cluster_role, )?; - namespace::maybe_patch_namespace(&namespace, &mut obj, &gvk)?; env::maybe_copy_env(&deployment, &mut obj, &gvk)?; resources::maybe_copy_resources(&deployment, &mut obj, &gvk)?; // ---------- apply diff --git a/rust/olm-deployer/src/namespace/mod.rs b/rust/olm-deployer/src/namespace/mod.rs deleted file mode 100644 index 8324b5b5..00000000 --- a/rust/olm-deployer/src/namespace/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -use anyhow::Result; -use serde_json::{Value, json}; -use stackable_operator::kube::api::{DynamicObject, GroupVersionKind}; - -use crate::data; - -/// Path the namespace of the autoTls secret class. -/// Otherwise do nothing. -pub(super) fn maybe_patch_namespace( - ns: &str, - res: &mut DynamicObject, - gvk: &GroupVersionKind, -) -> Result<()> { - if gvk.kind == "SecretClass" { - *auto_tls_namespace(&mut res.data)? = json!(ns.to_string()); - } - Ok(()) -} - -fn auto_tls_namespace(value: &mut serde_json::Value) -> Result<&mut Value> { - data::data_field_as_mut(value, "/spec/backend/autoTls/ca/secret/namespace") -} - -#[cfg(test)] -mod test { - use std::sync::LazyLock; - - use anyhow::Result; - use serde::Deserialize; - - use super::*; - - static TLS_SECRET_CLASS: LazyLock = LazyLock::new(|| { - const STR_TLS_SECRET_CLASS: &str = r#" ---- -apiVersion: secrets.stackable.tech/v1alpha1 -kind: SecretClass -metadata: - name: tls - labels: - app.kubernetes.io/name: secret-operator - app.kubernetes.io/instance: secret-operator - stackable.tech/vendor: Stackable - app.kubernetes.io/version: "24.11.0" -spec: - backend: - autoTls: - ca: - secret: - name: secret-provisioner-tls-ca - namespace: "${NAMESPACE}" # TODO patch with olm-deployer - autoGenerate: true -"#; - - let data = serde_yaml::Value::deserialize(serde_yaml::Deserializer::from_str( - STR_TLS_SECRET_CLASS, - )) - .unwrap(); - serde_yaml::from_value(data).unwrap() - }); - - #[test] - fn test_patch_namespace() -> Result<()> { - let gvk: GroupVersionKind = GroupVersionKind { - kind: "SecretClass".to_string(), - version: "v1alpha1".to_string(), - group: "secrets.stackable.tech".to_string(), - }; - let mut tls = TLS_SECRET_CLASS.clone(); - maybe_patch_namespace("prod", &mut tls, &gvk)?; - - let expected = json!("prod"); - assert_eq!(auto_tls_namespace(&mut tls.data)?, &expected); - Ok(()) - } -} diff --git a/rust/olm-deployer/src/resources/mod.rs b/rust/olm-deployer/src/resources/mod.rs index 47182f01..2daf5fcb 100644 --- a/rust/olm-deployer/src/resources/mod.rs +++ b/rust/olm-deployer/src/resources/mod.rs @@ -6,23 +6,28 @@ use stackable_operator::{ }, }; -use crate::data::container; +use crate::data::containers; /// Copies the resources of the container named "secret-operator-deployer" from `source` -/// to the container "secret-operator" in `target`. -/// Does nothing if there are no resources or if the `target` is not a DaemonSet. +/// to *all* containers in `target`. +/// Does nothing if there are no resources or if the `target` is not a DaemonSet or a Deployment. +/// This function allows OLM Subscription objects to configure the resources +/// of operator containers. pub(super) fn maybe_copy_resources( source: &Deployment, target: &mut DynamicObject, target_gvk: &GroupVersionKind, ) -> anyhow::Result<()> { - if target_gvk.kind == "DaemonSet" { + let target_kind_set = ["DaemonSet", "Deployment"]; + if target_kind_set.contains(&target_gvk.kind.as_str()) { if let Some(res) = deployment_resources(source) { - match container(target, "secret-operator")? { - serde_json::Value::Object(c) => { - c.insert("resources".to_string(), serde_json::json!(res)); + for container in containers(target)? { + match container { + serde_json::Value::Object(c) => { + c.insert("resources".to_string(), serde_json::json!(res)); + } + _ => anyhow::bail!("no containers found in object {}", target.name_any()), } - _ => anyhow::bail!("no containers found in object {}", target.name_any()), } } } @@ -150,7 +155,9 @@ spec: ..ResourceRequirements::default() }); assert_eq!( - container(&mut daemonset, "secret-operator")? + containers(&mut daemonset)? + .first() + .expect("daemonset has no containers") .get("resources") .unwrap(), &expected diff --git a/rust/olm-deployer/src/tolerations/mod.rs b/rust/olm-deployer/src/tolerations/mod.rs index c8588bdd..9f73562c 100644 --- a/rust/olm-deployer/src/tolerations/mod.rs +++ b/rust/olm-deployer/src/tolerations/mod.rs @@ -7,13 +7,16 @@ use crate::data::get_or_create; /// Copies the pod tolerations from the `source` to the `target`. /// Does nothing if there are no tolerations or if the `target` is not -/// a DaemonSet. +/// a DaemonSet or a Deployment. +/// Admins can configure Subscription tolerations when installing the operator +/// and these need to be copied over to the objects created by the deployer. pub(super) fn maybe_copy_tolerations( source: &Deployment, target: &mut DynamicObject, target_gvk: &GroupVersionKind, ) -> anyhow::Result<()> { - if target_gvk.kind == "DaemonSet" { + let target_kind_set = ["DaemonSet", "Deployment"]; + if target_kind_set.contains(&target_gvk.kind.as_str()) { if let Some(tolerations) = deployment_tolerations(source) { let path = "template/spec/tolerations".split("/"); *get_or_create(target.data.pointer_mut("/spec").unwrap(), path)? = serde_json::json!(