Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ All notable changes to this project will be documented in this file.
- Bump `stackable-operator` to 0.114.0 ([#867]).
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
functions and carry the full set of recommended labels ([#861]).
- The reconciler now applies resources and derives the cluster status in discrete
apply and update_status steps for the `opa_controller` ([#872]).

[#852]: https://github.com/stackabletech/opa-operator/pull/852
[#861]: https://github.com/stackabletech/opa-operator/pull/861
[#867]: https://github.com/stackabletech/opa-operator/pull/867
[#872]: https://github.com/stackabletech/opa-operator/pull/872

## [26.7.0] - 2026-07-21

Expand Down
173 changes: 173 additions & 0 deletions rust/operator-binary/src/controller/apply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//! The apply step in the OpaCluster controller.

use std::marker::PhantomData;

use serde_json::json;
use snafu::{ResultExt, Snafu};
use stackable_operator::{
client::Client,
cluster_resources::{ClusterResource, ClusterResourceApplyStrategy, ClusterResources},
deep_merger::ObjectOverrides,
k8s_openapi::api::apps::v1::DaemonSet,
kube::ResourceExt,
v2::cluster_resources::cluster_resources_new,
};
use strum::{EnumDiscriminants, IntoStaticStr};

use crate::controller::{
Applied, KubernetesResources, Prepared, ValidatedCluster, controller_name, operator_name,
product_name,
};

#[derive(Snafu, Debug, EnumDiscriminants)]
#[strum_discriminants(derive(IntoStaticStr))]
pub enum Error {
#[snafu(display("failed to apply Kubernetes resource"))]
ApplyResource {
source: stackable_operator::cluster_resources::Error,
},

#[snafu(display("failed to apply legacy field-manager patch for DaemonSet {name}"))]
ApplyPatchDaemonSet {
source: stackable_operator::client::Error,
name: String,
},

#[snafu(display("failed to delete orphaned resources"))]
DeleteOrphanedResources {
source: stackable_operator::cluster_resources::Error,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;

/// Applier for the Kubernetes resource specifications produced by this controller.
///
/// The implementation is not tied to this controller and could theoretically be moved to
/// stackable_operator if [`KubernetesResources`] would contain all possible resource types.
pub struct Applier<'a> {
client: &'a Client,
cluster_resources: ClusterResources<'a>,
}

impl<'a> Applier<'a> {
pub fn new(
client: &'a Client,
cluster: &ValidatedCluster,
apply_strategy: ClusterResourceApplyStrategy,
object_overrides: &'a ObjectOverrides,
) -> Applier<'a> {
let cluster_resources = cluster_resources_new(
&product_name(),
&operator_name(),
&controller_name(),
&cluster.name,
&cluster.namespace,
&cluster.uid,
apply_strategy,
object_overrides,
);

Applier {
client,
cluster_resources,
}
}

/// Applies the given Kubernetes resources and marks them as applied.
pub async fn apply(
mut self,
resources: KubernetesResources<Prepared>,
) -> Result<KubernetesResources<Applied>> {
// Destructured without `..`, so adding a field to [`KubernetesResources`] fails to
// compile here instead of silently never being applied.
let KubernetesResources {
daemon_sets,
services,
config_maps,
service_accounts,
role_bindings,
status: _,
} = resources;

// Apply order is: DaemonSets last (a changed mounted ConfigMap must exist first, else the
// Pods restart a second time -- commons-operator#111). The ServiceAccount comes first
// because the Pods reference it at creation time.
let service_accounts = self.add_resources(service_accounts).await?;
let role_bindings = self.add_resources(role_bindings).await?;
let services = self.add_resources(services).await?;
let config_maps = self.add_resources(config_maps).await?;
let daemon_sets = self.add_resources(daemon_sets).await?;

self.remove_legacy_field_manager_scope(&daemon_sets).await?;

self.cluster_resources
.delete_orphaned_resources(self.client)
.await
.context(DeleteOrphanedResourcesSnafu)?;

Ok(KubernetesResources {
daemon_sets,
services,
config_maps,
service_accounts,
role_bindings,
status: PhantomData,
})
}

async fn add_resources<T: ClusterResource + Sync>(
&mut self,
resources: Vec<T>,
) -> Result<Vec<T>> {
let mut applied_resources = vec![];

for resource in resources {
let applied_resource = self
.cluster_resources
.add(self.client, resource)
.await
.context(ApplyResourceSnafu)?;
applied_resources.push(applied_resource);
}

Ok(applied_resources)
}

/// Relinquishes the fields still owned by the historical field manager scope "opacluster".
///
/// A previous version of opa-operator used the field manager scope "opacluster" to write out a
/// DaemonSet with the bundle-builder container called "opa-bundle-builder". During
/// https://github.com/stackabletech/opa-operator/pull/420 it was renamed to "bundle-builder".
/// As we are now using the field manager scope "opa.stackable.tech_opacluster", our old changes
/// (with the old container) would stay valid. We have to use the old field manager scope and
/// post an empty patch to get rid of it.
/// https://github.com/stackabletech/issues/issues/390 will implement a proper fix, which also
/// covers Services and ConfigMaps. For details see
/// https://github.com/stackabletech/opa-operator/issues/444.
async fn remove_legacy_field_manager_scope(&self, daemon_sets: &[DaemonSet]) -> Result<()> {
for daemon_set in daemon_sets {
tracing::trace!(
"Removing old field manager scope \"opacluster\" of DaemonSet {daemonset_name} to remove the \"opa-bundle-builder\" container. \
See https://github.com/stackabletech/opa-operator/issues/444 and https://github.com/stackabletech/issues/issues/390 for details.",
daemonset_name = daemon_set.name_any()
);

self.client
.apply_patch(
"opacluster",
daemon_set,
// We can hardcode this here, as https://github.com/stackabletech/issues/issues/390
// will solve the general problem and we always have created DaemonSets using
// the "apps/v1" version.
json!({"apiVersion": "apps/v1", "kind": "DaemonSet"}),
)
.await
.context(ApplyPatchDaemonSetSnafu {
name: daemon_set.name_any(),
})?;
}

Ok(())
}
}
7 changes: 4 additions & 3 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Build steps that turn the [`ValidatedCluster`](super::ValidatedCluster) into
//! Kubernetes resource specifications.

use std::str::FromStr;
use std::{marker::PhantomData, str::FromStr};

use snafu::{ResultExt, Snafu};
use stackable_operator::{
Expand All @@ -11,7 +11,7 @@ use stackable_operator::{
};

use crate::controller::{
KubernetesResources, RoleGroupName, ValidatedCluster,
KubernetesResources, Prepared, RoleGroupName, ValidatedCluster,
build::resource::{
config_map::build_rolegroup_config_map,
daemonset::build_server_rolegroup_daemonset,
Expand Down Expand Up @@ -59,7 +59,7 @@ pub fn build(
opa_bundle_builder_image: &str,
user_info_fetcher_image: &str,
cluster_info: &KubernetesClusterInfo,
) -> Result<KubernetesResources, Error> {
) -> Result<KubernetesResources<Prepared>, Error> {
let mut daemon_sets = vec![];
let mut services = vec![];
let mut config_maps = vec![];
Expand Down Expand Up @@ -103,6 +103,7 @@ pub fn build(
config_maps,
service_accounts: vec![build_service_account(cluster)],
role_bindings: vec![build_role_binding(cluster)],
status: PhantomData,
})
}

Expand Down
17 changes: 15 additions & 2 deletions rust/operator-binary/src/controller/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Controller-level vocabulary: the [`ValidatedCluster`] type and the `build` / `validate`
//! sub-modules.

use std::{collections::BTreeMap, str::FromStr};
use std::{collections::BTreeMap, marker::PhantomData, str::FromStr};

// Re-exported so the rest of the controller refers to `crate::controller::RoleGroupName`.
pub use stackable_operator::v2::types::operator::RoleGroupName;
Expand Down Expand Up @@ -41,7 +41,9 @@ use crate::{
opa_controller::OPA_CONTROLLER_NAME,
};

pub mod apply;
pub mod build;
pub mod update_status;
pub mod validate;

/// The validated [`v1alpha2::OpaCluster`].
Expand Down Expand Up @@ -235,18 +237,29 @@ impl KubeResource for ValidatedCluster {
}
}

/// Marker for prepared Kubernetes resources which are not applied yet.
pub struct Prepared;

/// Marker for applied Kubernetes resources.
pub struct Applied;

/// Every Kubernetes resource produced by the [`build`](build::build) step.
///
/// OPA runs as a `DaemonSet` (one Pod per node), so there are no `StatefulSet`s, PDBs or
/// `Listener`s. `services` holds the role-level `Service` and the per-role-group headless and
/// metrics `Service`s; `config_maps` holds the per-role-group `ConfigMap`s and the cluster-level
/// discovery `ConfigMap`.
pub struct KubernetesResources {
///
/// `T` is a marker that indicates whether these resources are only [`Prepared`] or already
/// [`Applied`]. It lets the type system prove that e.g. the cluster status is derived from
/// applied resources rather than merely built ones.
pub struct KubernetesResources<T> {
pub daemon_sets: Vec<DaemonSet>,
pub services: Vec<Service>,
pub config_maps: Vec<ConfigMap>,
pub service_accounts: Vec<ServiceAccount>,
pub role_bindings: Vec<RoleBinding>,
pub status: PhantomData<T>,
}

/// Cluster-wide settings resolved once during validation, so the build steps no longer need the
Expand Down
55 changes: 55 additions & 0 deletions rust/operator-binary/src/controller/update_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! The update_status step in the OpaCluster controller.

use snafu::{ResultExt, Snafu};
use stackable_operator::{
client::Client,
status::condition::{
compute_conditions, daemonset::DaemonSetConditionBuilder,
operations::ClusterOperationsConditionBuilder,
},
};
use strum::{EnumDiscriminants, IntoStaticStr};

use crate::{
controller::{Applied, KubernetesResources},
crd::{OPERATOR_NAME, OpaClusterStatus, v1alpha2},
};

#[derive(Snafu, Debug, EnumDiscriminants)]
#[strum_discriminants(derive(IntoStaticStr))]
pub enum Error {
#[snafu(display("failed to update status"))]
ApplyStatus {
source: stackable_operator::client::Error,
},
}

type Result<T, E = Error> = std::result::Result<T, E>;

/// Computes the cluster status from the applied resources and patches it onto the
/// [`v1alpha2::OpaCluster`]. Takes [`KubernetesResources<Applied>`] so the type system proves the
/// status derives from applied resources, not merely built ones.
pub async fn update_status(
client: &Client,
opa: &v1alpha2::OpaCluster,
applied: &KubernetesResources<Applied>,
) -> Result<()> {
let mut ds_cond_builder = DaemonSetConditionBuilder::default();
for daemon_set in &applied.daemon_sets {
ds_cond_builder.add(daemon_set.clone());
}

let cluster_operation_cond_builder =
ClusterOperationsConditionBuilder::new(&opa.spec.cluster_operation);

let status = OpaClusterStatus {
conditions: compute_conditions(opa, &[&ds_cond_builder, &cluster_operation_cond_builder]),
};

client
.apply_patch_status(OPERATOR_NAME, opa, &status)
.await
.context(ApplyStatusSnafu)?;

Ok(())
}
Loading
Loading