|
| 1 | +use std::{hash::Hasher, marker::PhantomData}; |
| 2 | + |
| 3 | +use fnv::FnvHasher; |
| 4 | +use snafu::{ResultExt, Snafu}; |
| 5 | +use stackable_operator::{ |
| 6 | + builder::{configmap::ConfigMapBuilder, meta::ObjectMetaBuilder}, |
| 7 | + cluster_resources::ClusterResources, |
| 8 | + crd::listener::v1alpha1::Listener, |
| 9 | + k8s_openapi::api::core::v1::ConfigMap, |
| 10 | + kvp::{Labels, ObjectLabels}, |
| 11 | +}; |
| 12 | +use strum::{EnumDiscriminants, IntoStaticStr}; |
| 13 | + |
| 14 | +use super::{Applied, HIVE_CONTROLLER_NAME, KubernetesResources, Prepared, ValidatedHiveCluster}; |
| 15 | +use crate::{OPERATOR_NAME, crd::APP_NAME, listener::build_listener_connection_string}; |
| 16 | + |
| 17 | +#[derive(Snafu, Debug, EnumDiscriminants)] |
| 18 | +#[strum_discriminants(derive(IntoStaticStr))] |
| 19 | +pub enum Error { |
| 20 | + #[snafu(display("failed to apply service account"))] |
| 21 | + ApplyServiceAccount { |
| 22 | + source: stackable_operator::cluster_resources::Error, |
| 23 | + }, |
| 24 | + |
| 25 | + #[snafu(display("failed to apply role binding"))] |
| 26 | + ApplyRoleBinding { |
| 27 | + source: stackable_operator::cluster_resources::Error, |
| 28 | + }, |
| 29 | + |
| 30 | + #[snafu(display("failed to apply ConfigMap for {name}"))] |
| 31 | + ApplyConfigMap { |
| 32 | + source: stackable_operator::cluster_resources::Error, |
| 33 | + name: String, |
| 34 | + }, |
| 35 | + |
| 36 | + #[snafu(display("failed to apply Service for {name}"))] |
| 37 | + ApplyService { |
| 38 | + source: stackable_operator::cluster_resources::Error, |
| 39 | + name: String, |
| 40 | + }, |
| 41 | + |
| 42 | + #[snafu(display("failed to apply StatefulSet for {name}"))] |
| 43 | + ApplyStatefulSet { |
| 44 | + source: stackable_operator::cluster_resources::Error, |
| 45 | + name: String, |
| 46 | + }, |
| 47 | + |
| 48 | + #[snafu(display("failed to apply PodDisruptionBudget"))] |
| 49 | + ApplyPdb { |
| 50 | + source: stackable_operator::cluster_resources::Error, |
| 51 | + }, |
| 52 | + |
| 53 | + #[snafu(display("failed to apply listener for {role}"))] |
| 54 | + ApplyListener { |
| 55 | + source: stackable_operator::cluster_resources::Error, |
| 56 | + role: String, |
| 57 | + }, |
| 58 | + |
| 59 | + #[snafu(display("failed to build discovery ConfigMap"))] |
| 60 | + BuildDiscoveryConfigMap { |
| 61 | + source: stackable_operator::builder::configmap::Error, |
| 62 | + }, |
| 63 | + |
| 64 | + #[snafu(display("failed to apply discovery ConfigMap"))] |
| 65 | + ApplyDiscoveryConfigMap { |
| 66 | + source: stackable_operator::cluster_resources::Error, |
| 67 | + }, |
| 68 | + |
| 69 | + #[snafu(display("failed to build discovery ConfigMap metadata"))] |
| 70 | + DiscoveryMetadataBuild { |
| 71 | + source: stackable_operator::builder::meta::Error, |
| 72 | + }, |
| 73 | + |
| 74 | + #[snafu(display("failed to configure listener connection string"))] |
| 75 | + ListenerConfiguration { source: crate::listener::Error }, |
| 76 | + |
| 77 | + #[snafu(display("failed to delete orphaned resources"))] |
| 78 | + DeleteOrphanedResources { |
| 79 | + source: stackable_operator::cluster_resources::Error, |
| 80 | + }, |
| 81 | +} |
| 82 | + |
| 83 | +pub struct Applier<'a> { |
| 84 | + client: &'a stackable_operator::client::Client, |
| 85 | + cluster_resources: ClusterResources<'a>, |
| 86 | +} |
| 87 | + |
| 88 | +impl<'a> Applier<'a> { |
| 89 | + pub fn new( |
| 90 | + client: &'a stackable_operator::client::Client, |
| 91 | + cluster_resources: ClusterResources<'a>, |
| 92 | + ) -> Self { |
| 93 | + Self { |
| 94 | + client, |
| 95 | + cluster_resources, |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + pub async fn apply( |
| 100 | + mut self, |
| 101 | + prepared: KubernetesResources<Prepared>, |
| 102 | + validated: &ValidatedHiveCluster, |
| 103 | + ) -> Result<KubernetesResources<Applied>, Error> { |
| 104 | + // RBAC |
| 105 | + for sa in prepared.service_accounts { |
| 106 | + self.cluster_resources |
| 107 | + .add(self.client, sa) |
| 108 | + .await |
| 109 | + .context(ApplyServiceAccountSnafu)?; |
| 110 | + } |
| 111 | + for rb in prepared.role_bindings { |
| 112 | + self.cluster_resources |
| 113 | + .add(self.client, rb) |
| 114 | + .await |
| 115 | + .context(ApplyRoleBindingSnafu)?; |
| 116 | + } |
| 117 | + |
| 118 | + // ConfigMaps |
| 119 | + for cm in prepared.config_maps { |
| 120 | + let name = cm.metadata.name.clone().unwrap_or_default(); |
| 121 | + self.cluster_resources |
| 122 | + .add(self.client, cm) |
| 123 | + .await |
| 124 | + .context(ApplyConfigMapSnafu { name })?; |
| 125 | + } |
| 126 | + |
| 127 | + // Services |
| 128 | + for svc in prepared.services { |
| 129 | + let name = svc.metadata.name.clone().unwrap_or_default(); |
| 130 | + self.cluster_resources |
| 131 | + .add(self.client, svc) |
| 132 | + .await |
| 133 | + .context(ApplyServiceSnafu { name })?; |
| 134 | + } |
| 135 | + |
| 136 | + // StatefulSets — applied after ConfigMaps to prevent unnecessary Pod restarts |
| 137 | + let mut applied_stateful_sets = Vec::new(); |
| 138 | + for ss in prepared.stateful_sets { |
| 139 | + let name = ss.metadata.name.clone().unwrap_or_default(); |
| 140 | + let applied = self |
| 141 | + .cluster_resources |
| 142 | + .add(self.client, ss) |
| 143 | + .await |
| 144 | + .context(ApplyStatefulSetSnafu { name })?; |
| 145 | + applied_stateful_sets.push(applied); |
| 146 | + } |
| 147 | + |
| 148 | + // PDBs |
| 149 | + for pdb in prepared.pod_disruption_budgets { |
| 150 | + self.cluster_resources |
| 151 | + .add(self.client, pdb) |
| 152 | + .await |
| 153 | + .context(ApplyPdbSnafu)?; |
| 154 | + } |
| 155 | + |
| 156 | + // Listeners + discovery ConfigMaps |
| 157 | + // Discovery ConfigMaps depend on applied Listener status (for connection string building). |
| 158 | + // This is hive-specific: the Listener must be applied first, then we read its status |
| 159 | + // to build the connection string in the discovery ConfigMap. |
| 160 | + let mut discovery_hash = FnvHasher::with_key(0); |
| 161 | + |
| 162 | + for listener in prepared.listeners { |
| 163 | + let role = listener |
| 164 | + .metadata |
| 165 | + .labels |
| 166 | + .as_ref() |
| 167 | + .and_then(|l| l.get("app.kubernetes.io/component")) |
| 168 | + .cloned() |
| 169 | + .unwrap_or_else(|| "unknown".to_string()); |
| 170 | + |
| 171 | + let applied_listener: Listener = self |
| 172 | + .cluster_resources |
| 173 | + .add(self.client, listener) |
| 174 | + .await |
| 175 | + .context(ApplyListenerSnafu { role: role.clone() })?; |
| 176 | + |
| 177 | + for discovery_cm in build_discovery_configmaps(validated, &role, applied_listener)? { |
| 178 | + let applied_cm = self |
| 179 | + .cluster_resources |
| 180 | + .add(self.client, discovery_cm) |
| 181 | + .await |
| 182 | + .context(ApplyDiscoveryConfigMapSnafu)?; |
| 183 | + if let Some(generation) = applied_cm.metadata.resource_version { |
| 184 | + discovery_hash.write(generation.as_bytes()); |
| 185 | + } |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + self.cluster_resources |
| 190 | + .delete_orphaned_resources(self.client) |
| 191 | + .await |
| 192 | + .context(DeleteOrphanedResourcesSnafu)?; |
| 193 | + |
| 194 | + Ok(KubernetesResources { |
| 195 | + stateful_sets: applied_stateful_sets, |
| 196 | + config_maps: vec![], |
| 197 | + services: vec![], |
| 198 | + service_accounts: vec![], |
| 199 | + role_bindings: vec![], |
| 200 | + pod_disruption_budgets: vec![], |
| 201 | + listeners: vec![], |
| 202 | + discovery_hash: Some(discovery_hash.finish().to_string()), |
| 203 | + _status: PhantomData, |
| 204 | + }) |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +fn build_discovery_configmaps( |
| 209 | + validated: &ValidatedHiveCluster, |
| 210 | + role: &str, |
| 211 | + listener: Listener, |
| 212 | +) -> Result<Vec<ConfigMap>, Error> { |
| 213 | + let name = validated.name.to_string(); |
| 214 | + |
| 215 | + let recommended_object_labels = ObjectLabels { |
| 216 | + owner: validated, |
| 217 | + app_name: APP_NAME, |
| 218 | + app_version: &validated.image.app_version_label_value, |
| 219 | + operator_name: OPERATOR_NAME, |
| 220 | + controller_name: HIVE_CONTROLLER_NAME, |
| 221 | + role, |
| 222 | + role_group: "discovery", |
| 223 | + }; |
| 224 | + |
| 225 | + let labels = Labels::recommended(&recommended_object_labels) |
| 226 | + .expect("Labels should be created because the cluster name is a valid label value"); |
| 227 | + |
| 228 | + let connection_string = build_listener_connection_string(listener, &role.to_string(), None) |
| 229 | + .context(ListenerConfigurationSnafu)?; |
| 230 | + |
| 231 | + let mut cm_builder = ConfigMapBuilder::new(); |
| 232 | + cm_builder.metadata( |
| 233 | + ObjectMetaBuilder::new() |
| 234 | + .name(&name) |
| 235 | + .namespace(&validated.namespace) |
| 236 | + .ownerreference_from_resource(validated, None, Some(true)) |
| 237 | + .context(DiscoveryMetadataBuildSnafu)? |
| 238 | + .with_labels(labels) |
| 239 | + .build(), |
| 240 | + ); |
| 241 | + cm_builder.add_data("HIVE", connection_string); |
| 242 | + |
| 243 | + let discovery_cm = cm_builder.build().context(BuildDiscoveryConfigMapSnafu)?; |
| 244 | + |
| 245 | + Ok(vec![discovery_cm]) |
| 246 | +} |
0 commit comments