Skip to content

Commit e888dc4

Browse files
committed
Added olm-deployer crate.
1 parent 7f66b1d commit e888dc4

File tree

10 files changed

+958
-1
lines changed

10 files changed

+958
-1
lines changed

Cargo.lock

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["rust/operator-binary", "rust/csi-grpc"]
2+
members = ["rust/operator-binary", "rust/csi-grpc", "rust/olm-deployer"]
33
resolver = "2"
44

55
[workspace.package]
@@ -21,6 +21,8 @@ pin-project = "1.1"
2121
prost = "0.13"
2222
prost-types = "0.13"
2323
serde = "1.0"
24+
serde_json = "1.0"
25+
serde_yaml = "0.9"
2426
snafu = "0.8"
2527
stackable-operator = { git = "https://github.com/stackabletech/operator-rs.git", tag = "stackable-operator-0.85.0" }
2628
strum = { version = "0.26", features = ["derive"] }
@@ -31,6 +33,7 @@ tonic = "0.12"
3133
tonic-build = "0.12"
3234
tonic-reflection = "0.12"
3335
tracing = "0.1.40"
36+
walkdir = "2.5.0"
3437

3538
[patch."https://github.com/stackabletech/operator-rs.git"]
3639
# stackable-operator = { path = "../operator-rs/crates/stackable-operator" }

rust/olm-deployer/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "olm-deployer"
3+
description = "OLM deployment helper."
4+
version.workspace = true
5+
authors.workspace = true
6+
license.workspace = true
7+
edition.workspace = true
8+
repository.workspace = true
9+
publish = false
10+
11+
[dependencies]
12+
anyhow.workspace = true
13+
clap.workspace = true
14+
tokio.workspace = true
15+
tracing.workspace = true
16+
stackable-operator.workspace = true
17+
serde.workspace = true
18+
serde_json.workspace = true
19+
serde_yaml.workspace = true
20+
walkdir.workspace = true
21+
22+
[build-dependencies]
23+
built.workspace = true

rust/olm-deployer/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
built::write_built_file().unwrap();
3+
}

rust/olm-deployer/src/data.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use stackable_operator::kube::{api::DynamicObject, ResourceExt};
2+
3+
pub fn container<'a>(
4+
target: &'a mut DynamicObject,
5+
container_name: &str,
6+
) -> anyhow::Result<&'a mut serde_json::Value> {
7+
let tname = target.name_any();
8+
let path = "template/spec/containers".split("/");
9+
match get_or_create(target.data.pointer_mut("/spec").unwrap(), path)? {
10+
serde_json::Value::Array(containers) => {
11+
for c in containers {
12+
if c.is_object() {
13+
if let Some(serde_json::Value::String(name)) = c.get("name") {
14+
if container_name == name {
15+
return Ok(c);
16+
}
17+
}
18+
} else {
19+
anyhow::bail!("container is not a object: {:?}", c);
20+
}
21+
}
22+
anyhow::bail!("container named {container_name} not found");
23+
}
24+
_ => anyhow::bail!("no containers found in object {tname}"),
25+
}
26+
}
27+
28+
/// Returns the object nested in `root` by traversing the `path` of nested keys.
29+
/// Creates any missing objects in path.
30+
/// In case of success, the returned value is either the existing object or
31+
/// serde_json::Value::Null.
32+
/// Returns an error if any of the nested objects has a type other than map.
33+
pub fn get_or_create<'a, 'b, I>(
34+
root: &'a mut serde_json::Value,
35+
path: I,
36+
) -> anyhow::Result<&'a mut serde_json::Value>
37+
where
38+
I: IntoIterator<Item = &'b str>,
39+
{
40+
let mut iter = path.into_iter();
41+
match iter.next() {
42+
None => Ok(root),
43+
Some(first) => {
44+
let new_root = get_or_insert_default_object(root, first)?;
45+
get_or_create(new_root, iter)
46+
}
47+
}
48+
}
49+
50+
/// Given a map object create or return the object corresponding to the given `key`.
51+
fn get_or_insert_default_object<'a>(
52+
value: &'a mut serde_json::Value,
53+
key: &str,
54+
) -> anyhow::Result<&'a mut serde_json::Value> {
55+
let map = match value {
56+
serde_json::Value::Object(map) => map,
57+
x @ serde_json::Value::Null => {
58+
*x = serde_json::json!({});
59+
x.as_object_mut().unwrap()
60+
}
61+
x => anyhow::bail!("invalid type {x:?}, expected map"),
62+
};
63+
Ok(map.entry(key).or_insert_with(|| serde_json::Value::Null))
64+
}

rust/olm-deployer/src/env/mod.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use stackable_operator::{
2+
k8s_openapi::api::{apps::v1::Deployment, core::v1::EnvVar},
3+
kube::{
4+
api::{DynamicObject, GroupVersionKind},
5+
ResourceExt,
6+
},
7+
};
8+
9+
use crate::data::container;
10+
11+
/// Copy the environment from the "listener-operator-deployer" container in `source`
12+
/// to the container "listener-operator" in `target`.
13+
/// The `target` must be a DaemonSet object otherwise this is a no-op.
14+
pub(super) fn maybe_copy_env(
15+
source: &Deployment,
16+
target: &mut DynamicObject,
17+
target_gvk: &GroupVersionKind,
18+
) -> anyhow::Result<()> {
19+
if target_gvk.kind == "DaemonSet" {
20+
if let Some(env) = deployer_env_var(source) {
21+
match container(target, "listener-operator")? {
22+
serde_json::Value::Object(c) => {
23+
let json_env = env
24+
.iter()
25+
.map(|e| serde_json::json!(e))
26+
.collect::<Vec<serde_json::Value>>();
27+
28+
match c.get_mut("env") {
29+
Some(env) => match env {
30+
v @ serde_json::Value::Null => {
31+
*v = serde_json::json!(json_env);
32+
}
33+
serde_json::Value::Array(container_env) => {
34+
container_env.extend_from_slice(&json_env)
35+
}
36+
_ => anyhow::bail!("env is not null or an array"),
37+
},
38+
None => {
39+
c.insert("env".to_string(), serde_json::json!(json_env));
40+
}
41+
}
42+
}
43+
_ => anyhow::bail!("no containers found in object {}", target.name_any()),
44+
}
45+
}
46+
}
47+
48+
Ok(())
49+
}
50+
51+
fn deployer_env_var(deployment: &Deployment) -> Option<&Vec<EnvVar>> {
52+
deployment
53+
.spec
54+
.as_ref()
55+
.and_then(|ds| ds.template.spec.as_ref())
56+
.map(|ts| ts.containers.iter())
57+
.into_iter()
58+
.flatten()
59+
.filter(|c| c.name == "listener-operator-deployer")
60+
.last()
61+
.and_then(|c| c.env.as_ref())
62+
}
63+
64+
#[cfg(test)]
65+
mod test {
66+
use std::sync::LazyLock;
67+
68+
use anyhow::Result;
69+
use serde::Deserialize;
70+
71+
use super::*;
72+
73+
static DAEMONSET: LazyLock<DynamicObject> = LazyLock::new(|| {
74+
const STR_DAEMONSET: &str = r#"
75+
---
76+
apiVersion: apps/v1
77+
kind: DaemonSet
78+
metadata:
79+
name: listener-operator-daemonset
80+
spec:
81+
template:
82+
spec:
83+
containers:
84+
- name: listener-operator
85+
image: "quay.io/stackable/listener-operator@sha256:bb5063aa67336465fd3fa80a7c6fd82ac6e30ebe3ffc6dba6ca84c1f1af95bfe"
86+
env:
87+
- name: NAME1
88+
value: value1
89+
"#;
90+
91+
let data =
92+
serde_yaml::Value::deserialize(serde_yaml::Deserializer::from_str(STR_DAEMONSET))
93+
.unwrap();
94+
serde_yaml::from_value(data).unwrap()
95+
});
96+
97+
static DEPLOYMENT: LazyLock<Deployment> = LazyLock::new(|| {
98+
const STR_DEPLOYMENT: &str = r#"
99+
---
100+
apiVersion: apps/v1
101+
kind: Deployment
102+
metadata:
103+
name: listener-operator-deployer
104+
uid: d9287d0a-3069-47c3-8c90-b714dc6d1af5
105+
spec:
106+
template:
107+
spec:
108+
containers:
109+
- name: listener-operator-deployer
110+
image: "quay.io/stackable/tools@sha256:bb02df387d8f614089fe053373f766e21b7a9a1ad04cb3408059014cb0f1388e"
111+
env:
112+
- name: NAME2
113+
value: value2
114+
tolerations:
115+
- key: keep-out
116+
value: "yes"
117+
operator: Equal
118+
effect: NoSchedule
119+
"#;
120+
121+
let data =
122+
serde_yaml::Value::deserialize(serde_yaml::Deserializer::from_str(STR_DEPLOYMENT))
123+
.unwrap();
124+
serde_yaml::from_value(data).unwrap()
125+
});
126+
127+
#[test]
128+
fn test_copy_env_var() -> Result<()> {
129+
let gvk: GroupVersionKind = GroupVersionKind {
130+
kind: "DaemonSet".to_string(),
131+
version: "v1".to_string(),
132+
group: "apps".to_string(),
133+
};
134+
135+
let mut daemonset = DAEMONSET.clone();
136+
137+
maybe_copy_env(&DEPLOYMENT, &mut daemonset, &gvk)?;
138+
139+
let expected = serde_json::json!(vec![
140+
EnvVar {
141+
name: "NAME1".to_string(),
142+
value: Some("value1".to_string()),
143+
..EnvVar::default()
144+
},
145+
EnvVar {
146+
name: "NAME2".to_string(),
147+
value: Some("value2".to_string()),
148+
..EnvVar::default()
149+
},
150+
]);
151+
assert_eq!(
152+
container(&mut daemonset, "listener-operator")?
153+
.get("env")
154+
.unwrap(),
155+
&expected
156+
);
157+
Ok(())
158+
}
159+
}

0 commit comments

Comments
 (0)