From 86dd4073627815e16c6f5bd1fef256c7c9cd7438 Mon Sep 17 00:00:00 2001 From: Niladri Halder Date: Sun, 24 Dec 2023 09:22:36 +0000 Subject: [PATCH] feat(upgrade-job): add migration for loki-stack - removes .loki-stack.loki.config.ingester.lifecycler.ring.kvstore as it does not exist in loki-stack v2.9.11 - adds set value for the grafana/loki container image tag - removes .loki-stack.promtail.config.snippets.extraClientConfigs as it does not exist in loki-stack v2.9.11 - removes .loki-stack.promtail.initContainer as it does not exist in loki-stack v2.9.11 - migrates .loki-stack.promtail.config.lokiAddress to .loki-stack.promtail.config.clients - adds yq command_output helper function - add set value for .loki-stack.promtail.readinessProbe.httpGet.path - migrate .loki-stack.promtail.config.snippets.extraClientConfigs to .loki-stack.promtail.config.clients Signed-off-by: Niladri Halder --- .../src/bin/upgrade-job/common/error.rs | 76 +++++- k8s/upgrade/src/bin/upgrade-job/helm/chart.rs | 245 ++++++++++++++---- .../src/bin/upgrade-job/helm/values.rs | 141 ++++++++-- .../src/bin/upgrade-job/helm/yaml/yq.rs | 162 +++++++----- 4 files changed, 495 insertions(+), 129 deletions(-) diff --git a/k8s/upgrade/src/bin/upgrade-job/common/error.rs b/k8s/upgrade/src/bin/upgrade-job/common/error.rs index 5ed8d9286..f2947022b 100644 --- a/k8s/upgrade/src/bin/upgrade-job/common/error.rs +++ b/k8s/upgrade/src/bin/upgrade-job/common/error.rs @@ -4,6 +4,7 @@ use crate::{ UMBRELLA_CHART_UPGRADE_DOCS_URL, }, events::event_recorder::EventNote, + helm::chart::PromtailConfigClient, }; use snafu::Snafu; use std::path::PathBuf; @@ -426,6 +427,49 @@ pub(crate) enum Error { note: EventNote, }, + /// Error in serializing a helm::chart::PromtailConfigClient to a JSON string. + #[snafu(display( + "Failed to serialize .loki-stack.promtail.config.client {:?}: {}", + object, + source + ))] + SerializePromtailConfigClientToJson { + source: serde_json::Error, + object: PromtailConfigClient, + }, + + /// Error in deserializing a promtail helm chart's deprecated extraClientConfig to a + /// serde_json::Value. + #[snafu(display( + "Failed to deserialize .loki-stack.promtail.config.snippets.extraClientConfig to a serde_json::Value {}: {}", + config, + source + ))] + DeserializePromtailExtraConfig { + source: serde_yaml::Error, + config: String, + }, + + /// Error in serializing a promtail helm chart's deprecated extraClientConfig, in a + /// serde_json::Value, to JSON. + #[snafu(display("Failed to serialize to JSON {:?}: {}", config, source))] + SerializePromtailExtraConfigToJson { + source: serde_json::Error, + config: serde_json::Value, + }, + + /// Error in serializing the deprecated config.snippets.extraClientConfig from the promtail + /// helm chart v3.11.0. + #[snafu(display( + "Failed to serialize object to a serde_json::Value {}: {}", + object, + source + ))] + SerializePromtailExtraClientConfigToJson { + source: serde_json::Error, + object: String, + }, + /// Error for when there are too many io-engine Pods in one single node; #[snafu(display("Too many io-engine Pods in Node '{}'", node_name))] TooManyIoEnginePods { node_name: String }, @@ -601,14 +645,40 @@ pub(crate) enum Error { std_err: String, }, - /// Error for when the yq command to update a yaml object returns an error. + /// Error for when the yq command to delete an object path returns an error. + #[snafu(display( + "`yq` delete-object-command returned an error,\ncommand: {},\nargs: {:?},\nstd_err: {}", + command, + args, + std_err, + ))] + YqDeleteObjectCommand { + command: String, + args: Vec, + std_err: String, + }, + + /// Error for when the yq command to append to an array returns an error. + #[snafu(display( + "`yq` append-to-array-command returned an error,\ncommand: {},\nargs: {:?},\nstd_err: {}", + command, + args, + std_err, + ))] + YqAppendToArrayCommand { + command: String, + args: Vec, + std_err: String, + }, + + /// Error for when the yq command to append to an object returns an error. #[snafu(display( - "`yq` set-object-command returned an error,\ncommand: {},\nargs: {:?},\nstd_err: {}", + "`yq` append-to-object-command returned an error,\ncommand: {},\nargs: {:?},\nstd_err: {}", command, args, std_err, ))] - YqSetObjCommand { + YqAppendToObjectCommand { command: String, args: Vec, std_err: String, diff --git a/k8s/upgrade/src/bin/upgrade-job/helm/chart.rs b/k8s/upgrade/src/bin/upgrade-job/helm/chart.rs index edad86521..4c4506a06 100644 --- a/k8s/upgrade/src/bin/upgrade-job/helm/chart.rs +++ b/k8s/upgrade/src/bin/upgrade-job/helm/chart.rs @@ -1,6 +1,6 @@ use crate::common::error::{ReadingFile, U8VectorToString, YamlParseFromFile, YamlParseFromSlice}; use semver::Version; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use snafu::ResultExt; use std::{fs::read, path::Path, str}; @@ -173,35 +173,57 @@ impl CoreValues { self.csi.node_nvme_io_timeout() } + /// This is a getter for the grafana/loki container's image tag from the loki helm chart. + pub(crate) fn loki_stack_loki_image_tag(&self) -> &str { + self.loki_stack.loki_image_tag() + } + /// This is a getter for the promtail scrapeConfigs. - pub(crate) fn loki_promtail_scrape_configs(&self) -> &str { + pub(crate) fn loki_stack_promtail_scrape_configs(&self) -> &str { self.loki_stack.promtail_scrape_configs() } + + pub(crate) fn loki_stack_promtail_loki_address(&self) -> &str { + self.loki_stack.deprecated_promtail_loki_address() + } + + pub(crate) fn loki_stack_promtail_config_file(&self) -> &str { + self.loki_stack.promtail_config_file() + } + + pub(crate) fn loki_stack_promtail_readiness_probe_path(&self) -> &str { + self.loki_stack.promtail_readiness_probe_path() + } + + /// This returns the config.snippets.extraClientConfigs from the promtail helm chart v3.11.0. + pub(crate) fn promtail_extra_client_configs(&self) -> &str { + self.loki_stack.deprecated_promtail_extra_client_configs() + } } /// This is used to deserialize the yaml object agents. #[derive(Deserialize)] -pub(crate) struct Agents { +struct Agents { ha: Ha, } impl Agents { /// This is a getter for state of the 'ha' feature (enabled/disabled). - pub(crate) fn ha_is_enabled(&self) -> bool { + fn ha_is_enabled(&self) -> bool { self.ha.enabled() } } /// This is used to deserialize the yaml object 'agents.ha'. #[derive(Deserialize)] -pub(crate) struct Ha { +struct Ha { enabled: bool, } impl Ha { /// This returns the value of 'ha.enabled' from the values set. Defaults to 'true' is absent /// from the yaml. - pub(crate) fn enabled(&self) -> bool { + fn enabled(&self) -> bool { self.enabled } } @@ -210,7 +232,7 @@ impl Ha { /// container images. #[derive(Deserialize)] #[serde(rename_all(deserialize = "camelCase"))] -pub(crate) struct Image { +struct Image { /// The container image tag. tag: String, /// This contains image tags set based on which PRODUCT repository the microservice originates @@ -221,22 +243,22 @@ pub(crate) struct Image { impl Image { /// This is a getter for the container image tag used across the helm chart release. - pub(crate) fn tag(&self) -> &str { + fn tag(&self) -> &str { self.tag.as_str() } /// This is a getter for the control-plane repoTag set on a helm chart. - pub(crate) fn control_plane_repotag(&self) -> &str { + fn control_plane_repotag(&self) -> &str { self.repo_tags.control_plane() } /// This is a getter for the data-plane repoTag set on a helm chart. - pub(crate) fn data_plane_repotag(&self) -> &str { + fn data_plane_repotag(&self) -> &str { self.repo_tags.data_plane() } /// This is a getter for the extensions repoTag set on a helm chart. - pub(crate) fn extensions_repotag(&self) -> &str { + fn extensions_repotag(&self) -> &str { self.repo_tags.extensions() } } @@ -245,7 +267,7 @@ impl Image { /// component. #[derive(Deserialize, Default)] #[serde(rename_all(deserialize = "camelCase"))] -pub(crate) struct RepoTags { +struct RepoTags { /// This member of repoTags is used to set image tags for components from the control-plane /// repo. control_plane: String, @@ -257,17 +279,17 @@ pub(crate) struct RepoTags { impl RepoTags { /// This is a getter for the control-plane image tag set on a helm chart. - pub(crate) fn control_plane(&self) -> &str { + fn control_plane(&self) -> &str { self.control_plane.as_str() } /// This is a getter for the data-plane image tag set on a helm chart. - pub(crate) fn data_plane(&self) -> &str { + fn data_plane(&self) -> &str { self.data_plane.as_str() } /// This is a getter for the extensions image tag set on a helm chart. - pub(crate) fn extensions(&self) -> &str { + fn extensions(&self) -> &str { self.extensions.as_str() } } @@ -276,14 +298,14 @@ impl RepoTags { /// io-engine DaemonSet. #[derive(Deserialize)] #[serde(rename_all(deserialize = "camelCase"))] -pub(crate) struct IoEngine { +struct IoEngine { /// Tracing Loglevel details for the io-engine DaemonSet Pods. log_level: String, } impl IoEngine { /// This is a getter for the io-engine DaemonSet Pod's tracing logLevel. - pub(crate) fn log_level(&self) -> &str { + fn log_level(&self) -> &str { self.log_level.as_str() } } @@ -291,7 +313,7 @@ impl IoEngine { /// This is used to deserialize the yaml object 'eventing', v2.3.0 has it disabled by default, /// the default thereafter has it enabled. #[derive(Deserialize, Default)] -pub(crate) struct Eventing { +struct Eventing { // This value is defaulted to 'false' when 'Eventing' is absent in the yaml. // This works fine because we don't use the serde deserialized values during // the values.yaml merge. The merge is done with 'yq'. These are assumed values, @@ -305,14 +327,14 @@ pub(crate) struct Eventing { impl Eventing { /// This is a predicate for the installation setting for eventing. - pub(crate) fn enabled(&self) -> bool { + fn enabled(&self) -> bool { self.enabled } } /// This is used to deserialize the yaml object 'csi'. #[derive(Deserialize)] -pub(crate) struct Csi { +struct Csi { /// This contains the image tags for the kubernetes-csi sidecar containers. image: CsiImage, /// This contains configuration for the CSI node. @@ -321,32 +343,32 @@ pub(crate) struct Csi { impl Csi { /// This is a getter for the sig-storage/csi-provisioner image tag. - pub(crate) fn provisioner_image_tag(&self) -> &str { + fn provisioner_image_tag(&self) -> &str { self.image.provisioner_tag() } /// This is a getter for the sig-storage/csi-attacher image tag. - pub(crate) fn attacher_image_tag(&self) -> &str { + fn attacher_image_tag(&self) -> &str { self.image.attacher_tag() } /// This is a getter for the sig-storage/csi-snapshotter image tag. - pub(crate) fn snapshotter_image_tag(&self) -> &str { + fn snapshotter_image_tag(&self) -> &str { self.image.snapshotter_tag() } /// This is a getter for the sig-storage/snapshot-controller image tag. - pub(crate) fn snapshot_controller_image_tag(&self) -> &str { + fn snapshot_controller_image_tag(&self) -> &str { self.image.snapshot_controller_tag() } /// This is a getter for the sig-storage/csi-node-driver-registrar image tag. - pub(crate) fn node_driver_registrar_image_tag(&self) -> &str { + fn node_driver_registrar_image_tag(&self) -> &str { self.image.node_driver_registrar_tag() } /// This is a getter for the CSI node NVMe io_timeout. - pub(crate) fn node_nvme_io_timeout(&self) -> &str { + fn node_nvme_io_timeout(&self) -> &str { self.node.nvme_io_timeout() } } @@ -354,7 +376,7 @@ impl Csi { /// This contains the image tags for the CSI sidecar containers. #[derive(Deserialize)] #[serde(rename_all(deserialize = "camelCase"))] -pub(crate) struct CsiImage { +struct CsiImage { /// This is the image tag for the csi-provisioner container. provisioner_tag: String, /// This is the image tag for the csi-attacher container. @@ -371,105 +393,240 @@ pub(crate) struct CsiImage { impl CsiImage { /// This is a getter for provisionerTag. - pub(crate) fn provisioner_tag(&self) -> &str { + fn provisioner_tag(&self) -> &str { self.provisioner_tag.as_str() } /// This is a getter for attacherTag. - pub(crate) fn attacher_tag(&self) -> &str { + fn attacher_tag(&self) -> &str { self.attacher_tag.as_str() } /// This is a getter for snapshotterTag. - pub(crate) fn snapshotter_tag(&self) -> &str { + fn snapshotter_tag(&self) -> &str { self.snapshotter_tag.as_str() } /// This is a getter for snapshotControllerTag. - pub(crate) fn snapshot_controller_tag(&self) -> &str { + fn snapshot_controller_tag(&self) -> &str { self.snapshot_controller_tag.as_str() } /// This is a getter for registrarTag. - pub(crate) fn node_driver_registrar_tag(&self) -> &str { + fn node_driver_registrar_tag(&self) -> &str { self.registrar_tag.as_str() } } /// This is used to deserialize the yaml object 'csi.node'. #[derive(Deserialize)] -pub(crate) struct CsiNode { +struct CsiNode { nvme: CsiNodeNvme, } impl CsiNode { /// This is a getter for the NVMe IO timeout. - pub(crate) fn nvme_io_timeout(&self) -> &str { + fn nvme_io_timeout(&self) -> &str { self.nvme.io_timeout() } } /// This is used to deserialize the yaml object 'csi.node.nvme'. #[derive(Deserialize)] -pub(crate) struct CsiNodeNvme { +struct CsiNodeNvme { io_timeout: String, } impl CsiNodeNvme { /// This is a getter for the IO timeout configuration. - pub(crate) fn io_timeout(&self) -> &str { + fn io_timeout(&self) -> &str { self.io_timeout.as_str() } } /// This is used to deserialize the yaml object 'loki-stack'. #[derive(Deserialize)] -pub(crate) struct LokiStack { +struct LokiStack { + loki: Loki, promtail: Promtail, } impl LokiStack { - pub(crate) fn promtail_scrape_configs(&self) -> &str { + /// This is a getter for the promtail scrapeConfigs value. + fn promtail_scrape_configs(&self) -> &str { self.promtail.scrape_configs() } + + /// This returns the config.snippets.extraClientConfigs from the promtail helm chart v3.11.0. + fn deprecated_promtail_extra_client_configs(&self) -> &str { + self.promtail.deprecated_extra_client_configs() + } + + /// This is a getter for the grafana/loki container's image tag. + fn loki_image_tag(&self) -> &str { + self.loki.image_tag() + } + + fn deprecated_promtail_loki_address(&self) -> &str { + self.promtail.deprecated_loki_address() + } + + fn promtail_config_file(&self) -> &str { + self.promtail.config_file() + } + + fn promtail_readiness_probe_path(&self) -> &str { + self.promtail.readiness_probe_http_get_path() + } +} + +/// This is used to deserialize the loki dependent helm chart's values set. +#[derive(Deserialize)] +struct Loki { + image: LokiImage, +} + +impl Loki { + /// This is a getter for the container image tag. + fn image_tag(&self) -> &str { + self.image.tag() + } +} + +/// This is used to deserialize the yaml object 'image' in the loki helm chart. +#[derive(Deserialize)] +struct LokiImage { + tag: String, +} + +impl LokiImage { + /// This is a getter for the image's tag. + fn tag(&self) -> &str { + self.tag.as_str() + } } /// This is used to deserialize the yaml object 'promtail'. #[derive(Deserialize)] -pub(crate) struct Promtail { +#[serde(rename_all(deserialize = "camelCase"))] +struct Promtail { config: PromtailConfig, + readiness_probe: PromtailReadinessProbe, } impl Promtail { /// This returns the promtail.config.snippets.scrapeConfigs as an &str. - pub(crate) fn scrape_configs(&self) -> &str { + fn scrape_configs(&self) -> &str { self.config.scrape_configs() } + + /// This returns the config.snippets.extraClientConfigs from the promtail helm chart v3.11.0. + fn deprecated_extra_client_configs(&self) -> &str { + self.config.deprecated_extra_client_configs() + } + + fn deprecated_loki_address(&self) -> &str { + self.config.deprecated_loki_address() + } + + fn config_file(&self) -> &str { + self.config.file() + } + + fn readiness_probe_http_get_path(&self) -> &str { + self.readiness_probe.http_get_path() + } } /// This is used to deserialize the promtail.config yaml object. #[derive(Deserialize)] -pub(crate) struct PromtailConfig { +struct PromtailConfig { + #[serde(default, rename(deserialize = "lokiAddress"))] + deprecated_loki_address: String, + file: String, snippets: PromtailConfigSnippets, } impl PromtailConfig { /// This returns the config.snippets.scrapeConfigs as an &str. - pub(crate) fn scrape_configs(&self) -> &str { + fn scrape_configs(&self) -> &str { self.snippets.scrape_configs() } + + /// This returns the snippets.extraClientConfigs from the promtail helm chart v3.11.0. + fn deprecated_extra_client_configs(&self) -> &str { + self.snippets.deprecated_extra_client_configs() + } + + /// This is a getter for the lokiAddress in the loki helm chart v2.6.4. + fn deprecated_loki_address(&self) -> &str { + self.deprecated_loki_address.as_str() + } + + fn file(&self) -> &str { + self.file.as_str() + } } /// This is used to deserialize the config.snippets yaml object. #[derive(Deserialize)] #[serde(rename_all(deserialize = "camelCase"))] -pub(crate) struct PromtailConfigSnippets { +struct PromtailConfigSnippets { + #[serde(default, rename(deserialize = "extraClientConfigs"))] + deprecated_extra_client_configs: String, scrape_configs: String, } impl PromtailConfigSnippets { /// This returns the snippets.scrapeConfigs as an &str. - pub(crate) fn scrape_configs(&self) -> &str { + fn scrape_configs(&self) -> &str { self.scrape_configs.as_str() } + + /// This returns the snippets.extraClientConfigs from the promtail helm chart v3.11.0. + fn deprecated_extra_client_configs(&self) -> &str { + self.deprecated_extra_client_configs.as_str() + } +} + +#[derive(Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +struct PromtailReadinessProbe { + http_get: PromtailReadinessProbeHttpGet, +} + +impl PromtailReadinessProbe { + fn http_get_path(&self) -> &str { + self.http_get.path() + } +} + +#[derive(Deserialize)] +struct PromtailReadinessProbeHttpGet { + path: String, +} + +impl PromtailReadinessProbeHttpGet { + fn path(&self) -> &str { + self.path.as_str() + } +} + +/// This is used to serialize the config.clients yaml object in promtail chart v6.13.1 +/// when migrating from promtail v3.11.0 to v6.13.1. +#[derive(Debug, Serialize)] +pub(crate) struct PromtailConfigClient { + url: String, +} + +impl PromtailConfigClient { + /// Create a new PromtailConfigClient with a url. + pub(crate) fn with_url(url: U) -> Self + where + U: ToString, + { + Self { + url: url.to_string(), + } + } } diff --git a/k8s/upgrade/src/bin/upgrade-job/helm/values.rs b/k8s/upgrade/src/bin/upgrade-job/helm/values.rs index 52c47e1bd..5829890d4 100644 --- a/k8s/upgrade/src/bin/upgrade-job/helm/values.rs +++ b/k8s/upgrade/src/bin/upgrade-job/helm/values.rs @@ -3,11 +3,14 @@ use crate::{ constants::{ TWO_DOT_FIVE, TWO_DOT_FOUR, TWO_DOT_ONE, TWO_DOT_O_RC_ONE, TWO_DOT_SIX, TWO_DOT_THREE, }, - error::{Result, SemverParse}, + error::{ + DeserializePromtailExtraConfig, Result, SemverParse, + SerializePromtailConfigClientToJson, SerializePromtailExtraConfigToJson, + }, file::write_to_tempfile, }, helm::{ - chart::CoreValues, + chart::{CoreValues, PromtailConfigClient}, yaml::yq::{YamlKey, YqV4}, }, }; @@ -81,7 +84,7 @@ where if source_values.io_engine_log_level().eq(log_level_to_replace) && target_values.io_engine_log_level().ne(log_level_to_replace) { - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".io_engine.logLevel")?, target_values.io_engine_log_level(), upgrade_values_file.path(), @@ -94,17 +97,17 @@ where // RepoTags fields will also be set to the values found in the target helm values file // (low_priority file). This is so integration tests which use specific repo commits can // upgrade to a custom helm chart. - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".image.repoTags.controlPlane")?, target_values.control_plane_repotag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".image.repoTags.dataPlane")?, target_values.data_plane_repotag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".image.repoTags.extensions")?, target_values.extensions_repotag(), upgrade_values_file.path(), @@ -124,7 +127,7 @@ where .eventing_enabled() .ne(&target_values.eventing_enabled()) { - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".eventing.enabled")?, target_values.eventing_enabled(), upgrade_values_file.path(), @@ -137,13 +140,14 @@ where })?; if source_version.ge(&two_dot_o_rc_zero) && source_version.lt(&two_dot_five) { // promtail + // TODO: check to see if it is the wrong one first. if source_values - .loki_promtail_scrape_configs() - .ne(target_values.loki_promtail_scrape_configs()) + .loki_stack_promtail_scrape_configs() + .ne(target_values.loki_stack_promtail_scrape_configs()) { - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".loki-stack.promtail.config.snippets.scrapeConfigs")?, - target_values.loki_promtail_scrape_configs(), + target_values.loki_stack_promtail_scrape_configs(), upgrade_values_file.path(), )?; } @@ -157,7 +161,7 @@ where .csi_node_nvme_io_timeout() .ne(io_timeout_to_replace) { - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.node.nvme.io_timeout")?, target_values.csi_node_nvme_io_timeout(), upgrade_values_file.path(), @@ -170,14 +174,36 @@ where version_string: TWO_DOT_SIX.to_string(), })?; if source_version.ge(&two_dot_o_rc_zero) && source_version.lt(&two_dot_six) { - yq.set_obj( - YamlKey::try_from(".loki-stack.loki")?, - target_values_filepath.as_ref(), + // Switch out image tag for the latest one. + yq.set_literal_value( + YamlKey::try_from(".loki-stack.loki.image.tag")?, + target_values.loki_stack_loki_image_tag(), + upgrade_values_file.path(), + )?; + // Delete deprecated objects. + yq.delete_object( + YamlKey::try_from(".loki-stack.loki.config.ingester.lifecycler.ring.kvstore")?, upgrade_values_file.path(), )?; - yq.set_obj( - YamlKey::try_from(".loki-stack.promtail")?, - target_values_filepath.as_ref(), + yq.delete_object( + YamlKey::try_from(".loki-stack.promtail.config.snippets.extraClientConfigs")?, + upgrade_values_file.path(), + )?; + yq.delete_object( + YamlKey::try_from(".loki-stack.promtail.initContainer")?, + upgrade_values_file.path(), + )?; + + loki_address_to_clients(target_values, upgrade_values_file.path(), &yq)?; + + yq.set_literal_value( + YamlKey::try_from(".loki-stack.promtail.config.file")?, + target_values.loki_stack_promtail_config_file(), + upgrade_values_file.path(), + )?; + yq.set_literal_value( + YamlKey::try_from(".loki-stack.promtail.readinessProbe.httpGet.path")?, + target_values.loki_stack_promtail_readiness_probe_path(), upgrade_values_file.path(), )?; } @@ -185,34 +211,34 @@ where // Default options. // Image tag is set because the high_priority file is the user's source options file. // The target's image tag needs to be set for PRODUCT upgrade. - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".image.tag")?, target_values.image_tag(), upgrade_values_file.path(), )?; // The CSI sidecar images need to always be the versions set on the chart by default. - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.image.provisionerTag")?, target_values.csi_provisioner_image_tag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.image.attacherTag")?, target_values.csi_attacher_image_tag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.image.snapshotterTag")?, target_values.csi_snapshotter_image_tag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.image.snapshotControllerTag")?, target_values.csi_snapshot_controller_image_tag(), upgrade_values_file.path(), )?; - yq.set_value( + yq.set_literal_value( YamlKey::try_from(".csi.image.registrarTag")?, target_values.csi_node_driver_registrar_image_tag(), upgrade_values_file.path(), @@ -223,3 +249,70 @@ where Ok(upgrade_values_file) } + +/// Converts config.lokiAddress and config.snippets.extraClientConfigs from the promtail helm chart +/// v3.11.0 to config.clients[] which is compatible with promtail helm chart v6.13.1. +fn loki_address_to_clients( + target_values: &CoreValues, + upgrade_values_filepath: &Path, + yq: &YqV4, +) -> Result<()> { + let promtail_config_clients_yaml_key = + YamlKey::try_from(".loki-stack.promtail.config.clients")?; + // Delete existing array, if any. The merge_files() should have added it with the default value + // set. + yq.delete_object( + promtail_config_clients_yaml_key.clone(), + upgrade_values_filepath, + )?; + let loki_address = target_values.loki_stack_promtail_loki_address(); + let promtail_config_client = PromtailConfigClient::with_url(loki_address); + let promtail_config_client = serde_json::to_string(&promtail_config_client).context( + SerializePromtailConfigClientToJson { + object: promtail_config_client, + }, + )?; + yq.append_to_array( + promtail_config_clients_yaml_key, + promtail_config_client, + upgrade_values_filepath, + )?; + // Merge the extraClientConfigs from the promtail v3.11.0 chart to the v6.13.1 chart's + // config.clients block. Ref: https://github.com/grafana/helm-charts/issues/1214 + // Ref: https://github.com/grafana/helm-charts/pull/1425 + if !target_values.promtail_extra_client_configs().is_empty() { + // Converting the YAML to a JSON because the yq command goes like this... + // yq '.config.clients[0] += {"tenant_id": "1", "basic_auth": {"username": "loki", + // "password": "secret"}}' file + let promtail_extra_client_config: serde_json::Value = serde_yaml::from_str( + target_values.promtail_extra_client_configs(), + ) + .context(DeserializePromtailExtraConfig { + config: target_values.promtail_extra_client_configs().to_string(), + })?; + let promtail_extra_client_config = serde_json::to_string(&promtail_extra_client_config) + .context(SerializePromtailExtraConfigToJson { + config: promtail_extra_client_config, + })?; + + yq.append_to_object( + YamlKey::try_from(".loki-stack.promtail.config.clients[0]")?, + promtail_extra_client_config, + upgrade_values_filepath, + )?; + } + + // Cleanup config.snippets.extraClientConfig from the promtail chart. + yq.delete_object( + YamlKey::try_from(".loki-stack.promtail.config.snippets.extraClientConfigs")?, + upgrade_values_filepath, + )?; + + // Cleanup config.lokiAddress from the promtail chart. + yq.delete_object( + YamlKey::try_from(".loki-stack.promtail.config.lokiAddress")?, + upgrade_values_filepath, + )?; + + Ok(()) +} diff --git a/k8s/upgrade/src/bin/upgrade-job/helm/yaml/yq.rs b/k8s/upgrade/src/bin/upgrade-job/helm/yaml/yq.rs index 9869d826a..16804e192 100644 --- a/k8s/upgrade/src/bin/upgrade-job/helm/yaml/yq.rs +++ b/k8s/upgrade/src/bin/upgrade-job/helm/yaml/yq.rs @@ -1,15 +1,23 @@ use crate::{ common::error::{ NotAValidYamlKeyForStringValue, NotYqV4, RegexCompile, Result, U8VectorToString, - YqCommandExec, YqMergeCommand, YqSetCommand, YqSetObjCommand, YqVersionCommand, + YqAppendToArrayCommand, YqAppendToObjectCommand, YqCommandExec, YqDeleteObjectCommand, + YqMergeCommand, YqSetCommand, YqVersionCommand, }, vec_to_strings, }; use regex::Regex; use snafu::{ensure, ResultExt}; -use std::{fmt::Display, ops::Deref, path::Path, process::Command, str}; +use std::{ + fmt::Display, + ops::Deref, + path::Path, + process::{Command, Output}, + str, +}; /// This is a container for the String of an input yaml key. +#[derive(Clone)] pub(crate) struct YamlKey(String); impl TryFrom<&str> for YamlKey { @@ -60,14 +68,7 @@ impl YqV4 { let yq_version_arg = "-V".to_string(); - let yq_version_output = yq_v4 - .command() - .arg(yq_version_arg.clone()) - .output() - .context(YqCommandExec { - command: yq_v4.command_as_str().to_string(), - args: vec![yq_version_arg.clone()], - })?; + let yq_version_output = yq_v4.command_output(vec![yq_version_arg.clone()])?; ensure!( yq_version_output.status.success(), @@ -98,6 +99,86 @@ impl YqV4 { Ok(yq_v4) } + /// Append objects to yaml arrays. + pub(crate) fn append_to_array(&self, key: YamlKey, value: V, filepath: P) -> Result<()> + where + V: Display + Sized, + P: AsRef, + { + let yq_append_to_array_args = vec_to_strings![ + "-i", + format!(r#"{} += [{value}]"#, key.as_str()), + filepath.as_ref().to_string_lossy() + ]; + let yq_append_to_array_output = self.command_output(yq_append_to_array_args.clone())?; + + ensure!( + yq_append_to_array_output.status.success(), + YqAppendToArrayCommand { + command: self.command_as_str().to_string(), + args: yq_append_to_array_args, + std_err: str::from_utf8(yq_append_to_array_output.stderr.as_slice()) + .context(U8VectorToString)? + .to_string() + } + ); + + Ok(()) + } + + /// Append objects to yaml arrays. + pub(crate) fn append_to_object(&self, key: YamlKey, value: V, filepath: P) -> Result<()> + where + V: Display + Sized, + P: AsRef, + { + let yq_append_to_object_args = vec_to_strings![ + "-i", + format!(r#"{} += {value}"#, key.as_str()), + filepath.as_ref().to_string_lossy() + ]; + let yq_append_to_object_output = self.command_output(yq_append_to_object_args.clone())?; + + ensure!( + yq_append_to_object_output.status.success(), + YqAppendToObjectCommand { + command: self.command_as_str().to_string(), + args: yq_append_to_object_args, + std_err: str::from_utf8(yq_append_to_object_output.stderr.as_slice()) + .context(U8VectorToString)? + .to_string() + } + ); + + Ok(()) + } + + /// Use the yq 'delpaths' operator to delete objects from a yaml file. + pub(crate) fn delete_object

(&self, key: YamlKey, filepath: P) -> Result<()> + where + P: AsRef, + { + let yq_delete_object_args = vec_to_strings![ + "-i", + format!(r#"delpaths([["{}"]])"#, key.as_str()), + filepath.as_ref().to_string_lossy() + ]; + let yq_delete_object_output = self.command_output(yq_delete_object_args.clone())?; + + ensure!( + yq_delete_object_output.status.success(), + YqDeleteObjectCommand { + command: self.command_as_str().to_string(), + args: yq_delete_object_args, + std_err: str::from_utf8(yq_delete_object_output.stderr.as_slice()) + .context(U8VectorToString)? + .to_string() + } + ); + + Ok(()) + } + // TODO: // 1. Arrays are treated like unique values on their own, and high_priority is preferred over // low_priority. Arrays are not merged, if the object in the array member is identical to an @@ -153,14 +234,7 @@ impl YqV4 { low_priority.as_ref().to_string_lossy(), high_priority.as_ref().to_string_lossy() ]; - let yq_merge_output = self - .command() - .args(yq_merge_args.clone()) - .output() - .context(YqCommandExec { - command: self.command_as_str().to_string(), - args: yq_merge_args.clone(), - })?; + let yq_merge_output = self.command_output(yq_merge_args.clone())?; ensure!( yq_merge_output.status.success(), @@ -177,7 +251,7 @@ impl YqV4 { } /// This sets in-place yaml values in yaml files. - pub(crate) fn set_value(&self, key: YamlKey, value: V, filepath: P) -> Result<()> + pub(crate) fn set_literal_value(&self, key: YamlKey, value: V, filepath: P) -> Result<()> where V: Display + Sized, P: AsRef, @@ -234,50 +308,22 @@ impl YqV4 { Ok(()) } - /// This sets yaml objects to a file from the same yaml object in another file. - pub(crate) fn set_obj(&self, key: YamlKey, obj_source: P, filepath: Q) -> Result<()> - where - P: AsRef, - Q: AsRef, - { - let yq_set_obj_args = vec_to_strings![ - "-i", - format!( - r#"{} = load("{}"){}"#, - key.as_str(), - obj_source.as_ref().to_string_lossy(), - key.as_str() - ), - filepath.as_ref().to_string_lossy() - ]; - let yq_set_obj_output = self - .command() - .args(yq_set_obj_args.clone()) - .output() - .context(YqCommandExec { - command: self.command_as_str().to_string(), - args: yq_set_obj_args.clone(), - })?; - - ensure!( - yq_set_obj_output.status.success(), - YqSetObjCommand { - command: self.command_as_str().to_string(), - args: yq_set_obj_args, - std_err: str::from_utf8(yq_set_obj_output.stderr.as_slice()) - .context(U8VectorToString)? - .to_string() - } - ); - - Ok(()) - } - /// Returns an std::process::Command using the command_as_str member's value. fn command(&self) -> Command { Command::new(self.command_name.clone()) } + /// This executes a command and returns the output. + fn command_output(&self, args: Vec) -> Result { + self.command() + .args(args.clone()) + .output() + .context(YqCommandExec { + command: self.command_as_str().to_string(), + args, + }) + } + /// The binary name of the `yq` command. fn command_as_str(&self) -> &str { self.command_name.as_str()