Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
163 changes: 161 additions & 2 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,104 @@

use std::str::FromStr;

use stackable_operator::v2::types::common::Port;
use snafu::{ResultExt, Snafu};
use stackable_operator::{utils::cluster_info::KubernetesClusterInfo, v2::types::common::Port};

use crate::controller::RoleGroupName;
use crate::controller::{
KubernetesResources, RoleGroupName, ValidatedCluster,
build::resource::{
config_map::build_rolegroup_config_map,
daemonset::build_server_rolegroup_daemonset,
discovery::build_discovery_config_map,
service::{
build_rolegroup_headless_service, build_rolegroup_metrics_service,
build_server_role_service,
},
},
};

pub mod properties;
pub mod resource;

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("failed to build ConfigMap for role group {role_group}"))]
ConfigMap {
source: resource::config_map::Error,
role_group: RoleGroupName,
},

#[snafu(display("failed to build DaemonSet for role group {role_group}"))]
DaemonSet {
source: resource::daemonset::Error,
role_group: RoleGroupName,
},

#[snafu(display("failed to build the discovery ConfigMap"))]
Discovery { source: resource::discovery::Error },
}

/// Builds every Kubernetes resource for the given validated cluster.
///
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
/// failures only.
///
/// `service_account_name` is the name of the RBAC `ServiceAccount` the Pods run under (the RBAC
/// resources are built and applied separately, in the reconcile step). `cluster_info` supplies the
/// Kubernetes cluster domain used in the discovery URL and the sidecar environment.
pub fn build(
cluster: &ValidatedCluster,
service_account_name: &str,
opa_bundle_builder_image: &str,
user_info_fetcher_image: &str,
cluster_info: &KubernetesClusterInfo,
) -> Result<KubernetesResources, Error> {
let mut daemon_sets = vec![];
let mut services = vec![];
let mut config_maps = vec![];

// The role-level load-balanced Service, which is not bound to a single role group.
services.push(build_server_role_service(cluster));

for role_group_configs in cluster.role_group_configs.values() {
for (role_group_name, role_group) in role_group_configs {
config_maps.push(
build_rolegroup_config_map(cluster, role_group_name, role_group).context(
ConfigMapSnafu {
role_group: role_group_name.clone(),
},
)?,
);
services.push(build_rolegroup_headless_service(cluster, role_group_name));
services.push(build_rolegroup_metrics_service(cluster, role_group_name));
daemon_sets.push(
build_server_rolegroup_daemonset(
cluster,
role_group_name,
role_group,
opa_bundle_builder_image,
user_info_fetcher_image,
service_account_name,
cluster_info,
)
.context(DaemonSetSnafu {
role_group: role_group_name.clone(),
})?,
);
}
}

// The cluster-level discovery ConfigMap.
config_maps.push(build_discovery_config_map(cluster, cluster_info).context(DiscoverySnafu)?);

Ok(KubernetesResources {
daemon_sets,
services,
config_maps,
})
}

/// The port the bundle-builder sidecar listens on, and which OPA connects to for bundle polling.
pub(crate) const BUNDLE_BUILDER_PORT: Port = Port(3030);

Expand All @@ -21,3 +112,71 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_ROLE_LEVEL_ROLE_GROUP: Role
// Placeholder role-group name for the recommended labels of the discovery `ConfigMap`, which is a
// cluster-level object not bound to a single role group.
stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleGroupName = "discovery");

#[cfg(test)]
mod tests {
use serde_json::json;
use stackable_operator::{
commons::networking::DomainName, kube::Resource, utils::cluster_info::KubernetesClusterInfo,
};

use super::build;
use crate::controller::{
ValidatedCluster, build::properties::test_support::validated_cluster_from_spec,
};

fn cluster_info() -> KubernetesClusterInfo {
KubernetesClusterInfo {
cluster_domain: DomainName::try_from("cluster.local").unwrap(),
}
}

fn sorted_names(resources: &[impl Resource]) -> Vec<&str> {
let mut names: Vec<&str> = resources
.iter()
.filter_map(|resource| resource.meta().name.as_deref())
.collect();
names.sort();
names
}

/// A validated single-role-group cluster with no TLS or extra sidecars.
fn cluster() -> ValidatedCluster {
validated_cluster_from_spec(json!({
"image": { "productVersion": "1.2.3" },
"servers": { "roleGroups": { "default": {} } },
}))
}

#[test]
fn build_produces_expected_resource_names() {
let resources = build(
&cluster(),
"test-opa-serviceaccount",
"bundle-builder-image",
"user-info-fetcher-image",
&cluster_info(),
)
.expect("build succeeds");

// One DaemonSet per role group.
assert_eq!(
sorted_names(&resources.daemon_sets),
["test-opa-server-default"]
);
// The role-level Service plus a headless and a metrics Service per role group.
assert_eq!(
sorted_names(&resources.services),
[
"test-opa-server",
"test-opa-server-default-headless",
"test-opa-server-default-metrics",
]
);
// The per-role-group ConfigMap plus the cluster-level discovery ConfigMap (`test-opa`).
assert_eq!(
sorted_names(&resources.config_maps),
["test-opa", "test-opa-server-default"]
);
}
}
26 changes: 5 additions & 21 deletions rust/operator-binary/src/controller/build/resource/daemonset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ use stackable_operator::{
apps::v1::{DaemonSet, DaemonSetSpec, DaemonSetUpdateStrategy, RollingUpdateDaemonSet},
core::v1::{
EmptyDirVolumeSource, EnvVarSource, HTTPGetAction, ObjectFieldSelector, Probe,
ResourceRequirements, SecretVolumeSource, ServiceAccount,
ResourceRequirements, SecretVolumeSource,
},
},
apimachinery::pkg::{apis::meta::v1::LabelSelector, util::intstr::IntOrString},
},
kube::ResourceExt,
memory::{BinaryMultiple, MemoryQuantity},
product_logging::{
self,
Expand Down Expand Up @@ -245,7 +244,7 @@ pub fn build_server_rolegroup_daemonset(
role_group: &OpaRoleGroupConfig,
opa_bundle_builder_image: &str,
user_info_fetcher_image: &str,
service_account: &ServiceAccount,
service_account_name: &str,
cluster_info: &KubernetesClusterInfo,
) -> Result<DaemonSet> {
let resolved_product_image = &cluster.image;
Expand Down Expand Up @@ -405,7 +404,7 @@ pub fn build_server_rolegroup_daemonset(
.build(),
)
.context(AddVolumeSnafu)?
.service_account_name(service_account.name_any())
.service_account_name(service_account_name)
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());

if let Some(tls) = &cluster.cluster_config.tls {
Expand Down Expand Up @@ -847,12 +846,7 @@ fn build_prepare_start_command(
#[cfg(test)]
mod tests {
use serde_json::json;
use stackable_operator::{
commons::networking::DomainName,
k8s_openapi::{
api::core::v1::ServiceAccount, apimachinery::pkg::apis::meta::v1::ObjectMeta,
},
};
use stackable_operator::commons::networking::DomainName;

use super::*;
use crate::{
Expand All @@ -865,16 +859,6 @@ mod tests {
}
}

fn service_account() -> ServiceAccount {
ServiceAccount {
metadata: ObjectMeta {
name: Some("test-opa-serviceaccount".to_owned()),
..Default::default()
},
..Default::default()
}
}

fn build(cluster: &ValidatedCluster) -> DaemonSet {
let (role_group_name, role_group) = cluster.role_group_configs[&OpaRole::Server]
.iter()
Expand All @@ -886,7 +870,7 @@ mod tests {
role_group,
"bundle-builder-image",
"user-info-fetcher-image",
&service_account(),
"test-opa-serviceaccount",
&cluster_info(),
)
.expect("the daemonset should build")
Expand Down
16 changes: 16 additions & 0 deletions rust/operator-binary/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use stackable_operator::{
product_image_selection::ResolvedProductImage,
resources::{NoRuntimeLimits, Resources},
},
k8s_openapi::api::{
apps::v1::DaemonSet,
core::v1::{ConfigMap, Service},
},
kube::{Resource as KubeResource, api::ObjectMeta},
kvp::Labels,
shared::time::Duration,
Expand Down Expand Up @@ -232,6 +236,18 @@ impl KubeResource for ValidatedCluster {
}
}

/// 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 {
pub daemon_sets: Vec<DaemonSet>,
pub services: Vec<Service>,
pub config_maps: Vec<ConfigMap>,
}

/// Cluster-wide settings resolved once during validation, so the build steps no longer need the
/// raw `OpaCluster` to render config (except for owner references).
pub struct ValidatedClusterConfig {
Expand Down
Loading
Loading