Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- BREAKING: Use `serde_json::Value` instead of `String` for user-provided JSON configOverrides. This changed is marked as breaking, as it causes a breaking change to the CRD ([#1206]).
Comment thread
sbernauer marked this conversation as resolved.
Outdated

[#1206]: https://github.com/stackabletech/operator-rs/pull/1206

## [0.111.1] - 2026-04-28

### Added
Expand Down
60 changes: 56 additions & 4 deletions crates/stackable-operator/crds/DummyCluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1007,8 +1007,34 @@ spec:
type: string
type: array
userProvided:
description: Override the entire config file with the specified String.
type: string
description: |-
Override the entire config file with the specified JSON document.

Please note that you can in-line JSON into YAML as follows:

```yaml
# ... other YAML content
userProvided: {
"myString": "test",
"myList": ["test"],
"myBool": true,
"my": {"nested.field.with.dots": 42}
}
```

As an alternative you can also stick to YAML:

```yaml
# ... other YAML content
userProvided:
myString: test
myList: [test]
myBool: true
my:
nested.field.with.dots: 42
```
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
dummy.properties:
additionalProperties:
Expand Down Expand Up @@ -1659,8 +1685,34 @@ spec:
type: string
type: array
userProvided:
description: Override the entire config file with the specified String.
type: string
description: |-
Override the entire config file with the specified JSON document.

Please note that you can in-line JSON into YAML as follows:

```yaml
# ... other YAML content
userProvided: {
"myString": "test",
"myList": ["test"],
"myBool": true,
"my": {"nested.field.with.dots": 42}
}
```

As an alternative you can also stick to YAML:

```yaml
# ... other YAML content
userProvided:
myString: test
myList: [test]
myBool: true
my:
nested.field.with.dots: 42
```
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
dummy.properties:
additionalProperties:
Expand Down
95 changes: 71 additions & 24 deletions crates/stackable-operator/src/config_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ pub enum Error {
source: serde_json::Error,
index: usize,
},

#[snafu(display("failed to parse user-provided JSON content"))]
ParseUserProvidedJson { source: serde_json::Error },
}

/// Trait that allows the product config pipeline to extract flat key-value
Expand Down Expand Up @@ -89,8 +86,33 @@ pub enum JsonConfigOverrides {
/// `{"op": "add", "path": "/0/happy", "value": true}`
JsonPatches(Vec<String>),

/// Override the entire config file with the specified String.
UserProvided(String),
/// Override the entire config file with the specified JSON document.
///
/// Please note that you can in-line JSON into YAML as follows:
///
/// ```yaml
/// # ... other YAML content
/// userProvided: {
/// "myString": "test",
/// "myList": ["test"],
/// "myBool": true,
/// "my": {"nested.field.with.dots": 42}
/// }
/// ```
///
/// As an alternative you can also stick to YAML:
///
/// ```yaml
/// # ... other YAML content
/// userProvided:
/// myString: test
/// myList: [test]
/// myBool: true
/// my:
/// nested.field.with.dots: 42
/// ```
#[schemars(schema_with = "raw_object_schema")]
UserProvided(serde_json::Value),
}

impl JsonConfigOverrides {
Expand Down Expand Up @@ -123,18 +145,18 @@ impl JsonConfigOverrides {
json_patch::patch(&mut doc, &operations).context(ApplyJsonPatchSnafu)?;
Ok(doc)
}
Self::UserProvided(content) => {
serde_json::from_str(content).context(ParseUserProvidedJsonSnafu)
}
Self::UserProvided(content) => Ok(content.clone()),
}
}
}

#[cfg(test)]
mod tests {
use rstest::rstest;
use serde_json::json;

use super::*;
use crate::utils::yaml_from_str_singleton_map;

#[test]
fn json_merge_patch_add_and_overwrite_fields() {
Expand Down Expand Up @@ -244,30 +266,16 @@ mod tests {
#[test]
fn user_provided_ignores_base() {
let base = json!({"foo": "bar"});
let content = "{\"custom\": true}";
let content = json!({"custom": true});

let overrides = JsonConfigOverrides::UserProvided(content.to_owned());
let overrides = JsonConfigOverrides::UserProvided(content);

let result = overrides
.apply(&base)
.expect("user provided should succeed");
assert_eq!(result, json!({"custom": true}));
}

#[test]
fn user_provided_invalid_json_returns_error() {
let base = json!({"foo": "bar"});

let overrides = JsonConfigOverrides::UserProvided("not valid json".to_owned());

let result = overrides.apply(&base);
assert!(
matches!(result.unwrap_err(), Error::ParseUserProvidedJson { source } if source.to_string()
== "expected ident at line 1 column 2"),
"invalid JSON should return an error"
);
}

#[test]
fn key_value_config_overrides_as_product_config_overrides() {
let mut overrides = BTreeMap::new();
Expand All @@ -281,4 +289,43 @@ mod tests {
assert_eq!(result.get("key1"), Some(&Some("value1".to_owned())));
assert_eq!(result.get("key2"), Some(&Some("value2".to_owned())));
}

#[rstest]
#[case::inline_json(
r#"
# ... other YAML content
userProvided: {
"myString": "test",
"myList": ["test"],
"myBool": true,
"my": {"nested.field.with.dots": 42}
}
"#
)]
#[case::inline_yaml(
"
# ... other YAML content
userProvided:
myString: test
myList: [test]
myBool: true
my:
nested.field.with.dots: 42
"
)]
fn parse_user_provided_json(#[case] yaml: String) {
let expected = json!({
"myString": "test",
"myList": ["test"],
"myBool": true,
"my": {"nested.field.with.dots": 42}
});
let json_config_overrides: JsonConfigOverrides =
yaml_from_str_singleton_map(&yaml).unwrap();

match json_config_overrides {
JsonConfigOverrides::UserProvided(value) => assert_eq!(value, expected),
_ => panic!("JsonConfigOverrides must be of type UserProvided"),
}
}
}
Loading