diff --git a/workspaces/api/storewolf/src/main.rs b/workspaces/api/storewolf/src/main.rs index aefe7ed1136..fe967d7b4d8 100644 --- a/workspaces/api/storewolf/src/main.rs +++ b/workspaces/api/storewolf/src/main.rs @@ -21,6 +21,7 @@ use std::os::unix::fs::symlink; use std::path::Path; use std::str::FromStr; use std::{env, fs, process}; +use toml::{map::Entry, Value}; use apiserver::datastore::key::{Key, KeyType}; use apiserver::datastore::serialization::{to_pairs, to_pairs_with_prefix}; @@ -57,8 +58,11 @@ mod error { source: DataStoreVersionError, }, - #[snafu(display("defaults.toml is not valid TOML: {}", source))] - DefaultsFormatting { source: toml::de::Error }, + #[snafu(display("{} is not valid TOML: {}", file, source))] + DefaultsFormatting { + file: String, + source: toml::de::Error, + }, #[snafu(display("defaults.toml is not a TOML table"))] DefaultsNotTable {}, @@ -72,6 +76,9 @@ mod error { #[snafu(display("defaults.toml's metadata has unexpected types"))] DefaultsMetadataUnexpectedFormat {}, + #[snafu(display("defaults.toml data types do not match types defined in current variant's override-defaults.toml"))] + DefaultsVariantDoesNotMatch {}, + #[snafu(display("Error querying datstore for populated keys: {}", source))] QueryData { source: datastore::Error }, @@ -274,6 +281,56 @@ fn parse_metadata_toml(md_toml_val: toml::Value) -> Result> Ok(def_metadatas) } +/// This modifies the first given toml Value by inserting any values from the second Value. +/// +/// This is done recursively. Any time a scalar or array is seen, the left side is set to the +/// right side. Any time a table is seen, we iterate through the keys of the tables; if the left +/// side does not have the key from the right side, it's inserted, otherwise we recursively merge +/// the values in each table for that key. +/// +/// If at any point in the recursion the data types of the two values does not match, we error. +fn merge_values<'a>(merge_into: &'a mut Value, merge_from: &'a Value) -> Result<()> { + // If the types of left and right don't match, we have inconsistent models, and shouldn't try + // to merge them. + ensure!( + merge_into.same_type(&merge_from), + error::DefaultsVariantDoesNotMatch + ); + + match merge_from { + // If we see a scalar, we replace the left with the right. We treat arrays like scalars so + // behavior is clear - no question about whether we're appending right onto left, etc. + Value::String(_) + | Value::Integer(_) + | Value::Float(_) + | Value::Boolean(_) + | Value::Datetime(_) + | Value::Array(_) => *merge_into = merge_from.clone(), + + // If we see a table, we recursively merge each key. + Value::Table(t2) => { + // We know the other side is a table because of the `ensure` above. + let t1 = merge_into.as_table_mut().unwrap(); + for (k2, v2) in t2.iter() { + // Check if the left has the same key as the right. + match t1.entry(k2) { + // If not, we can just insert the value. + Entry::Vacant(e) => { + e.insert(v2.clone()); + } + // If so, we need to recursively merge; we don't want to replace an entire + // table, for example, because the left may have some distinct inner keys. + Entry::Occupied(ref mut e) => { + merge_values(e.get_mut(), v2)?; + } + } + } + } + } + + Ok(()) +} + /// Creates a new FilesystemDataStore at the given path, with data and metadata coming from /// defaults.toml at compile time. fn populate_default_datastore>( @@ -309,13 +366,24 @@ fn populate_default_datastore>( create_new_datastore(&base_path, version)?; } - // Read and parse defaults - let defaults_str = include_str!("../../../models/src/variant/current/defaults.toml"); + // Read and parse shared defaults + let defaults_str = include_str!("../../../models/defaults.toml"); let mut defaults_val: toml::Value = - toml::from_str(defaults_str).context(error::DefaultsFormatting)?; + toml::from_str(defaults_str).context(error::DefaultsFormatting { + file: "defaults.toml", + })?; + + // Merge in any defaults for the current variant + let variant_defaults_str = + include_str!("../../../models/src/variant/current/override-defaults.toml"); + let variant_defaults_val: toml::Value = + toml::from_str(variant_defaults_str).context(error::DefaultsFormatting { + file: "override_defaults.toml", + })?; + merge_values(&mut defaults_val, &variant_defaults_val)?; // Check if we have metadata and settings. If so, pull them out - // of `defaults_val` + // of `shared_defaults_val` let table = defaults_val .as_table_mut() .context(error::DefaultsNotTable)?; @@ -560,3 +628,46 @@ fn main() { process::exit(1); } } + +#[cfg(test)] +mod test { + use super::merge_values; + use toml::toml; + + #[test] + fn merge() { + let mut left = toml! { + top1 = "left top1" + top2 = "left top2" + [settings.inner] + inner_setting1 = "left inner_setting1" + inner_setting2 = "left inner_setting2" + }; + let right = toml! { + top1 = "right top1" + [settings] + setting = "right setting" + [settings.inner] + inner_setting1 = "right inner_setting1" + inner_setting3 = "right inner_setting3" + }; + // Can't comment inside this toml, unfortunately. + // "top1" is being overwritten from right. + // "top2" is only in the left and remains. + // "setting" is only in the right side. + // "inner" tests that recursion works; inner_setting1 is replaced, 2 is untouched, and + // 3 is new. + let expected = toml! { + top1 = "right top1" + top2 = "left top2" + [settings] + setting = "right setting" + [settings.inner] + inner_setting1 = "right inner_setting1" + inner_setting2 = "left inner_setting2" + inner_setting3 = "right inner_setting3" + }; + merge_values(&mut left, &right).unwrap(); + assert_eq!(left, expected); + } +} diff --git a/workspaces/models/src/aws-dev/defaults.toml b/workspaces/models/defaults.toml similarity index 84% rename from workspaces/models/src/aws-dev/defaults.toml rename to workspaces/models/defaults.toml index 4158e7cd21d..3faec0aaf70 100644 --- a/workspaces/models/src/aws-dev/defaults.toml +++ b/workspaces/models/defaults.toml @@ -1,5 +1,9 @@ -# OS-level defaults. -# Should match the structures in the model definition. +# Here we define default values for the settings in the API model. +# Variant builds can override or supplement these in +# src/VARIANT/override-defaults.toml. + +# The structures, fields, and types here need to match those of the API model, +# as defined in src/VARIANT/mod.rs. [settings] timezone = "America/Los_Angeles" @@ -28,7 +32,7 @@ restart-commands = [] [configuration-files.containerd-config-toml] path = "/etc/containerd/config.toml" -template-path = "/usr/share/templates/containerd-config-toml_aws-dev" +template-path = "/usr/share/templates/containerd-config-toml" # Updog. diff --git a/workspaces/models/src/aws-dev/override-defaults.toml b/workspaces/models/src/aws-dev/override-defaults.toml new file mode 100644 index 00000000000..6a28637912f --- /dev/null +++ b/workspaces/models/src/aws-dev/override-defaults.toml @@ -0,0 +1,3 @@ +[configuration-files.containerd-config-toml] +# No override to path +template-path = "/usr/share/templates/containerd-config-toml_aws-dev" diff --git a/workspaces/models/src/aws-k8s/defaults.toml b/workspaces/models/src/aws-k8s/defaults.toml deleted file mode 100644 index 797e3cff142..00000000000 --- a/workspaces/models/src/aws-k8s/defaults.toml +++ /dev/null @@ -1,112 +0,0 @@ -# OS-level defaults. -# Should match the structures in the model definition. - -[settings] -timezone = "America/Los_Angeles" -hostname = "localhost" - -[settings.updates] -metadata-base-url = "https://d25d9m6x9pxh9h.cloudfront.net/45efedef4afe/metadata/" -target-base-url = "https://d25d9m6x9pxh9h.cloudfront.net/45efedef4afe/targets/" - -[services.hostname] -configuration-files = ["hostname"] -restart-commands = [] - -[configuration-files.hostname] -path = "/etc/hostname" -template-path = "/usr/share/templates/hostname" - -[metadata.settings.hostname] -affected-services = ["hostname"] - -# Container runtime. - -[services.containerd] -configuration-files = ["containerd-config-toml"] -restart-commands = [] - -[configuration-files.containerd-config-toml] -path = "/etc/containerd/config.toml" -template-path = "/usr/share/templates/containerd-config-toml_aws-k8s" - -# Kubernetes. - -[services.kubernetes] -configuration-files = ["kubelet-env", "kubelet-config", "kubelet-kubeconfig", "kubernetes-ca-crt"] -restart-commands = [] - -[configuration-files.kubelet-env] -path = "/etc/kubernetes/kubelet/env" -template-path = "/usr/share/templates/kubelet-env" - -[configuration-files.kubelet-config] -path = "/etc/kubernetes/kubelet/config" -template-path = "/usr/share/templates/kubelet-config" - -[configuration-files.kubelet-kubeconfig] -path = "/etc/kubernetes/kubelet/kubeconfig" -template-path = "/usr/share/templates/kubelet-kubeconfig" - -[configuration-files.kubernetes-ca-crt] -path = "/etc/kubernetes/pki/ca.crt" -template-path = "/usr/share/templates/kubernetes-ca-crt" - -[metadata.settings.kubernetes] -max-pods.setting-generator = "pluto max-pods" -cluster-dns-ip.setting-generator = "pluto cluster-dns-ip" -node-ip.setting-generator = "pluto node-ip" -affected-services = ["kubernetes"] - -[metadata.settings.kubernetes.pod-infra-container-image] -setting-generator = "pluto pod-infra-container-image" -affected-services = ["kubernetes", "containerd"] - -# Updog. - -[services.updog] -configuration-files = ["updog-toml"] -restart-commands = [] - -[configuration-files.updog-toml] -path = "/etc/updog.toml" -template-path = "/usr/share/templates/updog-toml" - -[metadata.settings.updates] -affected-services = ["updog"] -seed.setting-generator = "bork seed" - -# HostContainers - -[settings.host-containers.admin] -enabled = false -source = "328549459982.dkr.ecr.us-west-2.amazonaws.com/thar-admin:v0.2" -superpowered = true - -[settings.host-containers.control] -enabled = true -source = "328549459982.dkr.ecr.us-west-2.amazonaws.com/thar-control:v0.2" -superpowered = false - -[services.host-containers] -configuration-files = [] -restart-commands = ["/usr/bin/host-containers"] - -[metadata.settings.host-containers] -affected-services = ["host-containers"] - -# NTP - -[settings.ntp] -time-servers = ["169.254.169.123", "2.amazon.pool.ntp.org"] - -[services.ntp] -configuration-files = ["chrony-conf"] -restart-commands = ["/bin/systemctl try-reload-or-restart chronyd.service"] - -[configuration-files.chrony-conf] -path = "/etc/chrony.conf" -template-path = "/usr/share/templates/chrony-conf" - -[metadata.settings.ntp] -affected-services = ["chronyd"] diff --git a/workspaces/models/src/aws-k8s/override-defaults.toml b/workspaces/models/src/aws-k8s/override-defaults.toml new file mode 100644 index 00000000000..48fd3ed0325 --- /dev/null +++ b/workspaces/models/src/aws-k8s/override-defaults.toml @@ -0,0 +1,35 @@ +[configuration-files.containerd-config-toml] +# No override to path +template-path = "/usr/share/templates/containerd-config-toml_aws-k8s" + +# Kubernetes. + +[services.kubernetes] +configuration-files = ["kubelet-env", "kubelet-config", "kubelet-kubeconfig", "kubernetes-ca-crt"] +restart-commands = [] + +[configuration-files.kubelet-env] +path = "/etc/kubernetes/kubelet/env" +template-path = "/usr/share/templates/kubelet-env" + +[configuration-files.kubelet-config] +path = "/etc/kubernetes/kubelet/config" +template-path = "/usr/share/templates/kubelet-config" + +[configuration-files.kubelet-kubeconfig] +path = "/etc/kubernetes/kubelet/kubeconfig" +template-path = "/usr/share/templates/kubelet-kubeconfig" + +[configuration-files.kubernetes-ca-crt] +path = "/etc/kubernetes/pki/ca.crt" +template-path = "/usr/share/templates/kubernetes-ca-crt" + +[metadata.settings.kubernetes] +max-pods.setting-generator = "pluto max-pods" +cluster-dns-ip.setting-generator = "pluto cluster-dns-ip" +node-ip.setting-generator = "pluto node-ip" +affected-services = ["kubernetes"] + +[metadata.settings.kubernetes.pod-infra-container-image] +setting-generator = "pluto pod-infra-container-image" +affected-services = ["kubernetes", "containerd"]