Skip to content

Commit 03eb858

Browse files
Validate environment variable names
1 parent 0c76fc4 commit 03eb858

5 files changed

Lines changed: 99 additions & 63 deletions

File tree

rust/operator-binary/src/controller/build/node_config.rs

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use super::ValidatedCluster;
55
use crate::{
66
controller::OpenSearchRoleGroupConfig,
77
crd::v1alpha1,
8-
framework::{builder::pod::container::EnvVarSet, role_group_utils},
8+
framework::{
9+
builder::pod::container::{EnvVarName, EnvVarSet},
10+
role_group_utils,
11+
},
912
};
1013

1114
pub const CONFIGURATION_FILE_OPENSEARCH_YML: &str = "opensearch.yml";
@@ -132,17 +135,20 @@ impl NodeConfig {
132135
EnvVarSet::new()
133136
// Set the OpenSearch node name to the Pod name.
134137
// The node name is used e.g. for `{INITIAL_CLUSTER_MANAGER_NODES}`.
135-
.with_field_path(CONFIG_OPTION_NODE_NAME, FieldPathEnvVar::Name)
138+
.with_field_path(
139+
EnvVarName::from_str_unsafe(CONFIG_OPTION_NODE_NAME),
140+
FieldPathEnvVar::Name,
141+
)
136142
.with_value(
137-
CONFIG_OPTION_DISCOVERY_SEED_HOSTS,
143+
EnvVarName::from_str_unsafe(CONFIG_OPTION_DISCOVERY_SEED_HOSTS),
138144
&self.discovery_service_name,
139145
)
140146
.with_value(
141-
CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES,
147+
EnvVarName::from_str_unsafe(CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES),
142148
self.initial_cluster_manager_nodes(),
143149
)
144150
.with_value(
145-
CONFIG_OPTION_NODE_ROLES,
151+
EnvVarName::from_str_unsafe(CONFIG_OPTION_NODE_ROLES),
146152
self.role_group_config
147153
.config
148154
.node_roles
@@ -153,7 +159,7 @@ impl NodeConfig {
153159
// is safe.
154160
.join(","),
155161
)
156-
.with_values(self.role_group_config.env_overrides.clone())
162+
.merge(self.role_group_config.env_overrides.clone())
157163
}
158164

159165
fn to_yaml(kv: serde_json::Map<String, Value>) -> String {
@@ -250,7 +256,7 @@ mod tests {
250256
affinity::StackableAffinity, product_image_selection::ProductImage,
251257
resources::Resources,
252258
},
253-
k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector, PodTemplateSpec},
259+
k8s_openapi::api::core::v1::PodTemplateSpec,
254260
kube::api::ObjectMeta,
255261
role_utils::GenericRoleConfig,
256262
};
@@ -289,7 +295,8 @@ mod tests {
289295
listener_class: "cluster-internal".to_string(),
290296
},
291297
config_overrides: HashMap::default(),
292-
env_overrides: [("TEST".to_owned(), "value".to_owned())].into(),
298+
env_overrides: EnvVarSet::new()
299+
.with_value(EnvVarName::from_str_unsafe("TEST"), "value"),
293300
cli_overrides: BTreeMap::default(),
294301
pod_overrides: PodTemplateSpec::default(),
295302
product_specific_common_config: GenericProductSpecificCommonConfig::default(),
@@ -303,44 +310,23 @@ mod tests {
303310

304311
let env_vars = node_config.environment_variables();
305312

306-
// TODO Test EnvVarSet and compare EnvVarSets
307313
assert_eq!(
308-
vec![
309-
EnvVar {
310-
name: "TEST".to_owned(),
311-
value: Some("value".to_owned()),
312-
value_from: None
313-
},
314-
EnvVar {
315-
name: "cluster.initial_cluster_manager_nodes".to_owned(),
316-
value: Some("".to_owned()),
317-
value_from: None
318-
},
319-
EnvVar {
320-
name: "discovery.seed_hosts".to_owned(),
321-
value: Some("my-opensearch-cluster-manager".to_owned()),
322-
value_from: None
323-
},
324-
EnvVar {
325-
name: "node.name".to_owned(),
326-
value: None,
327-
value_from: Some(EnvVarSource {
328-
config_map_key_ref: None,
329-
field_ref: Some(ObjectFieldSelector {
330-
api_version: None,
331-
field_path: "metadata.name".to_owned()
332-
}),
333-
resource_field_ref: None,
334-
secret_key_ref: None
335-
})
336-
},
337-
EnvVar {
338-
name: "node.roles".to_owned(),
339-
value: Some("".to_owned()),
340-
value_from: None
341-
}
342-
],
343-
<EnvVarSet as Into<Vec<EnvVar>>>::into(env_vars)
314+
EnvVarSet::new()
315+
.with_value(EnvVarName::from_str_unsafe("TEST"), "value",)
316+
.with_value(
317+
EnvVarName::from_str_unsafe("cluster.initial_cluster_manager_nodes"),
318+
"",
319+
)
320+
.with_value(
321+
EnvVarName::from_str_unsafe("discovery.seed_hosts"),
322+
"my-opensearch-cluster-manager",
323+
)
324+
.with_field_path(
325+
EnvVarName::from_str_unsafe("node.name"),
326+
FieldPathEnvVar::Name
327+
)
328+
.with_value(EnvVarName::from_str_unsafe("node.roles"), "",),
329+
env_vars
344330
);
345331
}
346332
}

rust/operator-binary/src/controller/build/role_group_builder.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::{
2323
crd::v1alpha1,
2424
framework::{
2525
RoleGroupName,
26-
builder::meta::ownerreference_from_resource,
26+
builder::{meta::ownerreference_from_resource, pod::container::EnvVarName},
2727
kvp::label::{recommended_labels, role_group_selector, role_selector},
2828
listener::listener_pvc,
2929
role_group_utils::ResourceNames,
@@ -275,13 +275,13 @@ impl<'a> RoleGroupBuilder<'a> {
275275

276276
// Use `OPENSEARCH_HOME` from envOverrides or default to `DEFAULT_OPENSEARCH_HOME`.
277277
let opensearch_home = env_vars
278-
.get_env_var("OPENSEARCH_HOME")
278+
.get(EnvVarName::from_str_unsafe("OPENSEARCH_HOME"))
279279
.and_then(|env_var| env_var.value.clone())
280280
.unwrap_or(DEFAULT_OPENSEARCH_HOME.to_owned());
281281
// Use `OPENSEARCH_PATH_CONF` from envOverrides or default to `{OPENSEARCH_HOME}/config`,
282282
// i.e. depend on `OPENSEARCH_HOME`.
283283
let opensearch_path_conf = env_vars
284-
.get_env_var("OPENSEARCH_PATH_CONF")
284+
.get(EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"))
285285
.and_then(|env_var| env_var.value.clone())
286286
.unwrap_or(format!("{opensearch_home}/config"));
287287

rust/operator-binary/src/controller/validate.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::{
1616
crd::v1alpha1::{self, OpenSearchConfig, OpenSearchConfigFragment},
1717
framework::{
1818
ClusterName,
19+
builder::pod::container::{EnvVarName, EnvVarSet},
1920
role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig, with_validated_config},
2021
},
2122
};
@@ -41,6 +42,11 @@ pub enum Error {
4142
#[snafu(display("failed to set role-group name"))]
4243
ParseRoleGroupName { source: crate::framework::Error },
4344

45+
#[snafu(display("failed to parse environment variable"))]
46+
ParseEnvironmentVariable {
47+
source: crate::framework::builder::pod::container::Error,
48+
},
49+
4450
#[snafu(display("fragment validation failure"))]
4551
ValidateOpenSearchConfig {
4652
source: stackable_operator::config::fragment::ValidationError,
@@ -125,12 +131,21 @@ fn validate_role_group_config(
125131
listener_class: merged_role_group.config.config.listener_class,
126132
};
127133

134+
let mut env_overrides = EnvVarSet::new();
135+
136+
for (env_var_name, env_var_value) in merged_role_group.config.env_overrides {
137+
env_overrides = env_overrides.with_value(
138+
EnvVarName::from_str(&env_var_name).context(ParseEnvironmentVariableSnafu)?,
139+
env_var_value,
140+
);
141+
}
142+
128143
Ok(RoleGroupConfig {
129144
// Kubernetes defaults to 1 if not set
130145
replicas: merged_role_group.replicas.unwrap_or(1),
131146
config: validated_config,
132147
config_overrides: merged_role_group.config.config_overrides,
133-
env_overrides: merged_role_group.config.env_overrides,
148+
env_overrides,
134149
cli_overrides: merged_role_group.config.cli_overrides,
135150
pod_overrides: merged_role_group.config.pod_overrides,
136151
product_specific_common_config: merged_role_group.config.product_specific_common_config,

rust/operator-binary/src/framework/builder/pod/container.rs

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,53 @@
1-
use std::collections::BTreeMap;
1+
use std::{collections::BTreeMap, fmt::Display, str::FromStr};
22

3+
use snafu::Snafu;
34
use stackable_operator::{
45
builder::pod::container::FieldPathEnvVar,
56
k8s_openapi::api::core::v1::{EnvVar, EnvVarSource, ObjectFieldSelector},
67
};
8+
use strum::{EnumDiscriminants, IntoStaticStr};
9+
10+
#[derive(Snafu, Debug, EnumDiscriminants)]
11+
#[strum_discriminants(derive(IntoStaticStr))]
12+
pub enum Error {
13+
#[snafu(display(
14+
"invalid environment variable name: a valid environment variable name must consist only of printable ASCII characters other than '='"
15+
))]
16+
ParseEnvVarName { env_var_name: String },
17+
}
18+
19+
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
20+
pub struct EnvVarName(String);
21+
22+
impl EnvVarName {
23+
pub fn from_str_unsafe(s: &str) -> Self {
24+
EnvVarName::from_str(s).expect("should be a valid environment variable name")
25+
}
26+
}
27+
28+
impl Display for EnvVarName {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
self.0.fmt(f)
31+
}
32+
}
33+
34+
impl FromStr for EnvVarName {
35+
type Err = Error;
736

8-
// TODO Use validated type
9-
type EnvVarName = String;
37+
fn from_str(s: &str) -> Result<Self, Self::Err> {
38+
// The length of the environment variable names seems not to be restricted.
39+
40+
if s.find(|c: char| !c.is_ascii_graphic() || c == '=')
41+
.is_none()
42+
{
43+
Ok(Self(s.to_owned()))
44+
} else {
45+
Err(Error::ParseEnvVarName {
46+
env_var_name: s.to_owned(),
47+
})
48+
}
49+
}
50+
}
1051

1152
#[derive(Clone, Debug, Default, PartialEq)]
1253
pub struct EnvVarSet(BTreeMap<EnvVarName, EnvVar>);
@@ -16,16 +57,10 @@ impl EnvVarSet {
1657
Self::default()
1758
}
1859

19-
pub fn get_env_var(&self, env_var_name: impl Into<EnvVarName>) -> Option<&EnvVar> {
60+
pub fn get(&self, env_var_name: impl Into<EnvVarName>) -> Option<&EnvVar> {
2061
self.0.get(&env_var_name.into())
2162
}
2263

23-
pub fn add_env_var(mut self, env_var: EnvVar) -> Self {
24-
self.0.insert(env_var.name.clone(), env_var);
25-
26-
self
27-
}
28-
2964
pub fn merge(mut self, mut env_var_set: EnvVarSet) -> Self {
3065
self.0.append(&mut env_var_set.0);
3166

@@ -51,7 +86,7 @@ impl EnvVarSet {
5186
self.0.insert(
5287
name.clone(),
5388
EnvVar {
54-
name,
89+
name: name.to_string(),
5590
value: Some(value.into()),
5691
value_from: None,
5792
},
@@ -70,7 +105,7 @@ impl EnvVarSet {
70105
self.0.insert(
71106
name.clone(),
72107
EnvVar {
73-
name,
108+
name: name.to_string(),
74109
value: None,
75110
value_from: Some(EnvVarSource {
76111
field_ref: Some(ObjectFieldSelector {

rust/operator-binary/src/framework/role_utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use stackable_operator::{
1111
schemars::JsonSchema,
1212
};
1313

14-
use super::ProductName;
14+
use super::{ProductName, builder::pod::container::EnvVarSet};
1515
use crate::framework::{ClusterName, MAX_OBJECT_NAME_LENGTH, kvp::label::MAX_LABEL_VALUE_LENGTH};
1616

1717
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
@@ -27,7 +27,7 @@ pub struct RoleGroupConfig<ProductSpecificCommonConfig, T> {
2727
pub replicas: u16,
2828
pub config: T,
2929
pub config_overrides: HashMap<String, HashMap<String, String>>,
30-
pub env_overrides: HashMap<String, String>,
30+
pub env_overrides: EnvVarSet,
3131
pub cli_overrides: BTreeMap<String, String>,
3232
pub pod_overrides: PodTemplateSpec,
3333
// allow(dead_code) is not necessary anymore when moved to operator-rs

0 commit comments

Comments
 (0)