Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Ensure that explicit YAML documents are generated for CRDs #450

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,13 +8,19 @@ All notable changes to this project will be documented in this file.

- Cluster resources can be added to a struct which determines the orphaned
resources and deletes them ([#436]).
- YAML module added with a function to serialize a data structure as an
explicit YAML document. The YAML documents generated by the functions in
`crd::CustomResourceExt` are now explicit documents and can be safely
concatenated to produce a YAML stream ([#450]).

### Changed

- BREAKING: The `managed_by` label must be passed explicitly to the
`ObjectMetaBuilder::with_recommended_labels` function ([#436]).
- serde\_yaml 0.8.26 -> 0.9 ([#450])

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

## [0.23.0] - 2022-07-26

Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Expand Up @@ -23,7 +23,7 @@ regex = "1.6.0"
schemars = "0.8.10"
serde = { version = "1.0.140", features = ["derive"] }
serde_json = "1.0.82"
serde_yaml = "0.8.26"
serde_yaml = "0.9"
strum = { version = "0.24.1", features = ["derive"] }
thiserror = "1.0.31"
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] }
Expand All @@ -39,7 +39,6 @@ stackable-operator-derive = { path = "stackable-operator-derive" }
[dev-dependencies]
rstest = "0.15.0"
tempfile = "3.3.0"
serde_yaml = "0.8"

[features]
default = ["native-tls"]
Expand Down
13 changes: 6 additions & 7 deletions src/cli.rs
Expand Up @@ -11,10 +11,10 @@
//! ```no_run
//! // Handle CLI arguments
//! use clap::{crate_version, Parser};
//! use kube::{CustomResource, CustomResourceExt};
//! use kube::CustomResource;
//! use schemars::JsonSchema;
//! use serde::{Deserialize, Serialize};
//! use stackable_operator::cli;
//! use stackable_operator::{CustomResourceExt, cli};
//! use stackable_operator::error::OperatorResult;
//!
//! #[derive(Clone, CustomResource, Debug, JsonSchema, Serialize, Deserialize)]
Expand Down Expand Up @@ -55,11 +55,10 @@
//! let opts = Opts::from_args();
//!
//! match opts.command {
//! cli::Command::Crd => println!(
//! "{}{}",
//! serde_yaml::to_string(&FooCluster::crd())?,
//! serde_yaml::to_string(&BarCluster::crd())?,
//! ),
//! cli::Command::Crd => {
//! FooCluster::print_yaml_schema()?;
//! BarCluster::print_yaml_schema()?;
//! },
//! cli::Command::Run { .. } => {
//! // Run the operator
//! }
Expand Down
12 changes: 6 additions & 6 deletions src/commons/s3.rs
Expand Up @@ -220,6 +220,7 @@ impl Default for S3AccessStyle {
mod test {
use crate::commons::s3::{S3AccessStyle, S3ConnectionDef};
use crate::commons::s3::{S3BucketSpec, S3ConnectionSpec};
use crate::yaml;

#[test]
fn test_ser_inline() {
Expand All @@ -235,14 +236,13 @@ mod test {
};

assert_eq!(
serde_yaml::to_string(&bucket).unwrap(),
yaml::to_explicit_document_string(&bucket).unwrap(),
"---
bucketName: test-bucket-name
connection:
inline:
host: host
port: 8080
accessStyle: VirtualHosted
connection: !inline
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, does this come from serde_yaml? We might need to go back to the old tagging format somehow…

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed solution in serde_yaml:0.9.4 is to annotate every enum field with #[serde(with = "serde_yaml::with::singleton_map")], see dtolnay/serde-yaml#300.

But this solution does not work together with JsonSchema, see GREsau/schemars#89.

One workaround is to use the serde parameters serialize_with and deserialize_with:

#[serde(
    default,
    skip_serializing_if = "Option::is_none",
    serialize_with = "serde_yaml::with::singleton_map::serialize",
    deserialize_with = "serde_yaml::with::singleton_map::deserialize"
)]
pub connection: Option<S3ConnectionDef>,

This becomes quite verbose.

In which cases do the YAML tags actually pose a problem?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serde_yaml:0.9.9 introduced singleton_map_recursive which allows to serialize enums as YAML maps containing one entry in which the key identifies the variant name.

This is now used in stackable_operator::yaml::serialize_to_explicit_document. Further annotations are not necessary in the operators.

host: host
port: 8080
accessStyle: VirtualHosted
"
.to_owned()
)
Expand Down
11 changes: 10 additions & 1 deletion src/crd.rs
Expand Up @@ -5,6 +5,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::{Error, OperatorResult};
use crate::yaml;
use std::fs::File;
use std::io::Write;
use std::path::Path;
Expand Down Expand Up @@ -72,28 +73,36 @@ pub trait HasApplication {
/// (e.g. creation) of `CustomResourceDefinition`s in Kubernetes.
pub trait CustomResourceExt: kube::CustomResourceExt {
/// Generates a YAML CustomResourceDefinition and writes it to a `Write`.
///
/// The generated YAML string is an explicit document with leading dashes (`---`).
fn generate_yaml_schema<W>(mut writer: W) -> OperatorResult<()>
where
W: Write,
{
let schema = serde_yaml::to_string(&Self::crd())?;
let schema = yaml::to_explicit_document_string(&Self::crd())?;
writer.write_all(schema.as_bytes())?;
Ok(())
}

/// Generates a YAML CustomResourceDefinition and writes it to the specified file.
///
/// The written YAML string is an explicit document with leading dashes (`---`).
fn write_yaml_schema<P: AsRef<Path>>(path: P) -> OperatorResult<()> {
let writer = File::create(path)?;
Self::generate_yaml_schema(writer)
}

/// Generates a YAML CustomResourceDefinition and prints it to stdout.
///
/// The printed YAML string is an explicit document with leading dashes (`---`).
fn print_yaml_schema() -> OperatorResult<()> {
let writer = std::io::stdout();
Self::generate_yaml_schema(writer)
}

// Returns the YAML schema of this CustomResourceDefinition as a string.
///
/// The written YAML string is an explicit document with leading dashes (`---`).
fn yaml_schema() -> OperatorResult<String> {
let mut writer = Vec::new();
Self::generate_yaml_schema(&mut writer)?;
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Expand Up @@ -9,6 +9,9 @@ pub enum Error {
source: serde_yaml::Error,
},

#[error("Failed to process YAML document: {message}")]
UnsupportedYamlDocumentError { message: String },

#[error("Kubernetes reported error: {source}")]
KubeError {
#[from]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -16,6 +16,7 @@ pub mod product_config_utils;
pub mod role_utils;
pub mod utils;
pub mod validation;
pub mod yaml;

pub use crate::crd::CustomResourceExt;

Expand Down
95 changes: 95 additions & 0 deletions src/yaml.rs
@@ -0,0 +1,95 @@
//! Utility functions for processing data in the YAML file format
use serde::ser;

use crate::error::{Error, OperatorResult};

/// A YAML document type
///
/// For a detailled description, see the
/// [YAML specification](https://yaml.org/spec/1.2.2/#rule-l-any-document).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DocumentType {
DirectiveDocument,
ExplicitDocument,
BareDocument,
}

/// Serializes the given data structure as an explicit YAML document.
///
/// # Errors
///
/// Serialization can fail if `T`'s implementation of `Serialize` decides to return an error.
///
/// An [`Error::UnsupportedYamlDocumentError`] is returned if the used `serde_yaml` version
/// generates a directive document.
pub fn to_explicit_document_string<T>(value: &T) -> OperatorResult<String>
where
T: ?Sized + ser::Serialize,
{
// The returned document type depends on the serde_yaml version.
let document = serde_yaml::to_string(value)?;
nightkr marked this conversation as resolved.
Show resolved Hide resolved

match determine_document_type(&document) {
DocumentType::DirectiveDocument => Err(Error::UnsupportedYamlDocumentError {
message: "serde_yaml::to_string generated a directive document which cannot be \
converted to an explicit document."
.into(),
}),
DocumentType::ExplicitDocument => Ok(document),
DocumentType::BareDocument => Ok(format!("---\n{}", document)),
}
}

/// Determines the type of the given YAML document.
///
/// It is assumend that the given string contains a valid YAML document.
pub fn determine_document_type(document: &str) -> DocumentType {
if document.starts_with('%') {
DocumentType::DirectiveDocument
} else if document.starts_with("---") {
DocumentType::ExplicitDocument
} else {
DocumentType::BareDocument
}
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use super::*;

#[test]
fn value_can_be_serialized_to_an_explicit_document_string() {
let value: BTreeMap<_, _> = [("key", "value")].into();

let actual_yaml = to_explicit_document_string(&value).expect("serializable value");

let expected_yaml = "\
---\n\
key: value\n";

assert_eq!(expected_yaml, actual_yaml);
}

#[test]
fn document_type_can_be_determined() {
let directive_document = "\
%YAML 1.2\n\
---\n\
key: value";
let explicit_document = "\
---\n\
key: value";
let bare_document = "\
key: value";

let directive_document_type = determine_document_type(directive_document);
let explicit_document_type = determine_document_type(explicit_document);
let bare_document_type = determine_document_type(bare_document);

assert_eq!(DocumentType::DirectiveDocument, directive_document_type);
assert_eq!(DocumentType::ExplicitDocument, explicit_document_type);
assert_eq!(DocumentType::BareDocument, bare_document_type);
}
}