use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec};
use k8s_openapi::api::core::v1::{Container, PodSpec, PodTemplateSpec, ServiceAccount};
use k8s_openapi::api::rbac::v1::{ClusterRole, ClusterRoleBinding, PolicyRule, RoleRef, Subject};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta};
use kube::CustomResourceExt;
use super::crd::{BoatRampCluster, Function, Site};
use super::Result;
const NAME: &str = "boatramp-operator";
#[derive(Debug, clap::Args)]
pub struct ManifestArgs {
#[arg(long, default_value = "boatramp-system")]
namespace: String,
#[arg(long, default_value = "ghcr.io/boatramp/boatramp:latest")]
image: String,
#[arg(long, default_value_t = 1)]
replicas: i32,
}
pub(crate) fn crds_yaml() -> Result<String> {
let mut out = String::new();
for crd in [BoatRampCluster::crd(), Site::crd(), Function::crd()] {
emit_to(&mut out, &crd)?;
}
Ok(out)
}
pub(crate) fn manifests_yaml(args: &ManifestArgs) -> Result<String> {
let mut out = crds_yaml()?;
emit_to(&mut out, &service_account(&args.namespace))?;
emit_to(&mut out, &cluster_role())?;
emit_to(&mut out, &cluster_role_binding(&args.namespace))?;
emit_to(
&mut out,
&deployment(&args.namespace, &args.image, args.replicas),
)?;
Ok(out)
}
pub fn print_crds() -> Result<()> {
print!("{}", crds_yaml()?);
Ok(())
}
pub fn print_manifests(args: &ManifestArgs) -> Result<()> {
print!("{}", manifests_yaml(args)?);
Ok(())
}
fn emit_to<T: serde::Serialize>(out: &mut String, obj: &T) -> Result<()> {
out.push_str("---\n");
let json = serde_json::to_string(obj)?;
let value: serde_yaml::Value = serde_yaml::from_str(&json)?;
out.push_str(&serde_yaml::to_string(&value)?);
Ok(())
}
fn labels() -> std::collections::BTreeMap<String, String> {
[
("app.kubernetes.io/name".to_string(), NAME.to_string()),
(
"app.kubernetes.io/managed-by".to_string(),
"boatramp".to_string(),
),
]
.into()
}
fn service_account(namespace: &str) -> ServiceAccount {
ServiceAccount {
metadata: ObjectMeta {
name: Some(NAME.to_string()),
namespace: Some(namespace.to_string()),
labels: Some(labels()),
..Default::default()
},
..Default::default()
}
}
fn cluster_role() -> ClusterRole {
let rule = |groups: &[&str], resources: &[&str], verbs: &[&str]| PolicyRule {
api_groups: Some(
groups
.iter()
.map(std::string::ToString::to_string)
.collect(),
),
resources: Some(
resources
.iter()
.map(std::string::ToString::to_string)
.collect(),
),
verbs: verbs.iter().map(std::string::ToString::to_string).collect(),
..Default::default()
};
let all = &[
"get", "list", "watch", "create", "update", "patch", "delete",
];
ClusterRole {
metadata: ObjectMeta {
name: Some(NAME.to_string()),
labels: Some(labels()),
..Default::default()
},
rules: Some(vec![
rule(
&["boatramp.dev"],
&[
"boatrampclusters",
"boatrampclusters/status",
"sites",
"sites/status",
"functions",
"functions/status",
],
all,
),
rule(&["apps"], &["statefulsets", "deployments"], all),
rule(
&[""],
&[
"services",
"configmaps",
"secrets",
"persistentvolumeclaims",
],
all,
),
rule(&["policy"], &["poddisruptionbudgets"], all),
rule(&["autoscaling"], &["horizontalpodautoscalers"], all),
rule(&[""], &["pods"], &["get", "list", "watch"]),
rule(&[""], &["events"], &["create", "patch"]),
]),
..Default::default()
}
}
fn cluster_role_binding(namespace: &str) -> ClusterRoleBinding {
ClusterRoleBinding {
metadata: ObjectMeta {
name: Some(NAME.to_string()),
labels: Some(labels()),
..Default::default()
},
role_ref: RoleRef {
api_group: "rbac.authorization.k8s.io".to_string(),
kind: "ClusterRole".to_string(),
name: NAME.to_string(),
},
subjects: Some(vec![Subject {
kind: "ServiceAccount".to_string(),
name: NAME.to_string(),
namespace: Some(namespace.to_string()),
..Default::default()
}]),
}
}
fn deployment(namespace: &str, image: &str, replicas: i32) -> Deployment {
Deployment {
metadata: ObjectMeta {
name: Some(NAME.to_string()),
namespace: Some(namespace.to_string()),
labels: Some(labels()),
..Default::default()
},
spec: Some(DeploymentSpec {
replicas: Some(replicas),
selector: LabelSelector {
match_labels: Some(labels()),
..Default::default()
},
template: PodTemplateSpec {
metadata: Some(ObjectMeta {
labels: Some(labels()),
..Default::default()
}),
spec: Some(PodSpec {
service_account_name: Some(NAME.to_string()),
containers: vec![Container {
name: "operator".to_string(),
image: Some(image.to_string()),
args: Some(vec!["operator".to_string(), "run".to_string()]),
..Default::default()
}],
..Default::default()
}),
},
..Default::default()
}),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crds_emit_all_three_kinds_as_yaml() {
for (crd, plural) in [
(BoatRampCluster::crd(), "boatrampclusters"),
(Site::crd(), "sites"),
(Function::crd(), "functions"),
] {
let yaml = serde_yaml::to_string(&crd).unwrap();
assert!(yaml.contains("kind: CustomResourceDefinition"), "{plural}");
assert!(yaml.contains("apiextensions.k8s.io/v1"), "{plural}");
assert!(yaml.contains(plural), "{plural} plural in schema");
assert!(yaml.contains("boatramp.dev"), "group");
}
}
#[test]
fn install_bundle_rbac_is_namespaced_and_scoped() {
let sa = serde_yaml::to_string(&service_account("boatramp-system")).unwrap();
assert!(sa.contains("kind: ServiceAccount") && sa.contains("boatramp-system"));
let role = serde_yaml::to_string(&cluster_role()).unwrap();
assert!(role.contains("boatrampclusters") && role.contains("boatramp.dev"));
assert!(!role.contains("'*'") && !role.contains("\"*\""));
let dep = serde_yaml::to_string(&deployment("boatramp-system", "img:test", 1)).unwrap();
assert!(dep.contains("kind: Deployment") && dep.contains("img:test"));
assert!(dep.contains("- operator") && dep.contains("- run"));
}
#[test]
fn cluster_role_rules_are_exactly_the_least_privilege_set() {
let s = |xs: &[&str]| {
xs.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
};
let all = s(&[
"get", "list", "watch", "create", "update", "patch", "delete",
]);
let got: Vec<(Vec<String>, Vec<String>, Vec<String>)> = cluster_role()
.rules
.unwrap()
.iter()
.map(|r| {
(
r.api_groups.clone().unwrap_or_default(),
r.resources.clone().unwrap_or_default(),
r.verbs.clone(),
)
})
.collect();
let expected = vec![
(
s(&["boatramp.dev"]),
s(&[
"boatrampclusters",
"boatrampclusters/status",
"sites",
"sites/status",
"functions",
"functions/status",
]),
all.clone(),
),
(
s(&["apps"]),
s(&["statefulsets", "deployments"]),
all.clone(),
),
(
s(&[""]),
s(&[
"services",
"configmaps",
"secrets",
"persistentvolumeclaims",
]),
all.clone(),
),
(s(&["policy"]), s(&["poddisruptionbudgets"]), all.clone()),
(
s(&["autoscaling"]),
s(&["horizontalpodautoscalers"]),
all.clone(),
),
(s(&[""]), s(&["pods"]), s(&["get", "list", "watch"])),
(s(&[""]), s(&["events"]), s(&["create", "patch"])),
];
assert_eq!(got, expected, "operator ClusterRole privileges changed");
}
#[test]
fn chart_crds_are_in_sync_with_the_rust_types() {
let generated = crds_yaml().unwrap();
let checked_in =
include_str!("../../../../charts/boatramp-operator/crds/boatramp-crds.yaml");
assert_eq!(
generated, checked_in,
"charts/boatramp-operator/crds/boatramp-crds.yaml is stale — regenerate it with \
`cargo run -p boatramp -- operator crds > charts/boatramp-operator/crds/boatramp-crds.yaml`"
);
}
}