Skip to content

Commit 5c1e86f

Browse files
authored
fix: idempotent helm installations for demos/stacks and ignoring of existing jobs (#386)
* helm idempotent installs * ignore jobs on reapply, merge instead of apply for existing objects * helm installs: uninstall and install chart instead of upgrade due to helmv3 force changes * jobs: change failed job apply output * remove playaround printing * jobs: restructure code * helm installs: add comment about procedure, cleanup * jobs: add comment for immutability error handling * add changelog entry
1 parent 5b03f54 commit 5c1e86f

File tree

4 files changed

+164
-9
lines changed

4 files changed

+164
-9
lines changed

rust/stackable-cockpit/src/helm.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ pub enum Error {
6161
#[snafu(display("failed to install Helm release"))]
6262
InstallRelease { source: InstallReleaseError },
6363

64+
#[snafu(display("failed to upgrade/install Helm release"))]
65+
UpgradeRelease { source: InstallReleaseError },
66+
6467
#[snafu(display("failed to uninstall Helm release ({error})"))]
6568
UninstallRelease { error: String },
6669
}
@@ -248,6 +251,78 @@ pub fn install_release_from_repo_or_registry(
248251
})
249252
}
250253

254+
/// Upgrades a Helm release from a repo or registry.
255+
///
256+
/// This function expects the fully qualified Helm release name. In case of our
257+
/// operators this is: `<PRODUCT_NAME>-operator`.
258+
#[instrument(skip(values_yaml), fields(with_values = values_yaml.is_some(), indicatif.pb_show = true))]
259+
pub fn upgrade_or_install_release_from_repo_or_registry(
260+
release_name: &str,
261+
ChartVersion {
262+
chart_source,
263+
chart_name,
264+
chart_version,
265+
}: ChartVersion,
266+
values_yaml: Option<&str>,
267+
namespace: &str,
268+
suppress_output: bool,
269+
) -> Result<InstallReleaseStatus, Error> {
270+
// Ideally, each Helm invocation would spawn_blocking instead in/around helm_sys,
271+
// but that requires a larger refactoring
272+
block_in_place(|| {
273+
debug!("Install/Upgrade Helm release from repo");
274+
Span::current()
275+
.pb_set_message(format!("Installing/Upgrading {chart_name} Helm chart").as_str());
276+
277+
if check_release_exists(release_name, namespace)? {
278+
let release = get_release(release_name, namespace)?.ok_or(Error::InstallRelease {
279+
source: InstallReleaseError::NoSuchRelease {
280+
name: release_name.to_owned(),
281+
},
282+
})?;
283+
284+
let current_version = release.version;
285+
286+
match chart_version {
287+
Some(chart_version) => {
288+
if chart_version == current_version {
289+
return Ok(InstallReleaseStatus::ReleaseAlreadyInstalledWithVersion {
290+
requested_version: chart_version.to_string(),
291+
release_name: release_name.to_string(),
292+
current_version,
293+
});
294+
}
295+
}
296+
None => {
297+
return Ok(InstallReleaseStatus::ReleaseAlreadyInstalledUnspecified {
298+
release_name: release_name.to_string(),
299+
current_version,
300+
});
301+
}
302+
}
303+
}
304+
305+
let full_chart_name = format!("{chart_source}/{chart_name}");
306+
let chart_version = chart_version.unwrap_or(HELM_DEFAULT_CHART_VERSION);
307+
308+
debug!(
309+
release_name,
310+
chart_version, full_chart_name, "Installing Helm release"
311+
);
312+
313+
upgrade_release(
314+
release_name,
315+
&full_chart_name,
316+
chart_version,
317+
values_yaml,
318+
namespace,
319+
suppress_output,
320+
)?;
321+
322+
Ok(InstallReleaseStatus::Installed(release_name.to_string()))
323+
})
324+
}
325+
251326
/// Installs a Helm release.
252327
///
253328
/// This function expects the fully qualified Helm release name. In case of our
@@ -281,6 +356,47 @@ fn install_release(
281356
Ok(())
282357
}
283358

359+
/// Upgrades a Helm release.
360+
/// If a release with the specified `chart_name` does not already exist,
361+
/// this function installs it instead.
362+
///
363+
/// This function expects the fully qualified Helm release name. In case of our
364+
/// operators this is: `<PRODUCT_NAME>-operator`.
365+
#[instrument(fields(with_values = values_yaml.is_some()))]
366+
fn upgrade_release(
367+
release_name: &str,
368+
chart_name: &str,
369+
chart_version: &str,
370+
values_yaml: Option<&str>,
371+
namespace: &str,
372+
suppress_output: bool,
373+
) -> Result<(), Error> {
374+
// In Helm 3 the behavior of the `--force` option has changed
375+
// It no longer deletes and re-installs a resource https://github.com/helm/helm/issues/7082#issuecomment-559558318
376+
// Because of that, conflict errors might appear, which fail the upgrade, even if `helm upgrade --force` is used
377+
// Therefore we uninstall the previous release (if present) and install the new one
378+
uninstall_release(release_name, namespace, suppress_output)?;
379+
380+
let result = helm_sys::install_helm_release(
381+
release_name,
382+
chart_name,
383+
chart_version,
384+
values_yaml.unwrap_or(""),
385+
namespace,
386+
suppress_output,
387+
);
388+
389+
if let Some(error) = helm_sys::to_helm_error(&result) {
390+
error!("Go wrapper function go_install_helm_release encountered an error: {error}");
391+
392+
return Err(Error::UpgradeRelease {
393+
source: InstallReleaseError::HelmWrapper { error },
394+
});
395+
}
396+
397+
Ok(())
398+
}
399+
284400
/// Uninstall a Helm release.
285401
///
286402
/// This function expects the fully qualified Helm release name. In case of our

rust/stackable-cockpit/src/platform/manifests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub trait InstallManifestsExt {
116116
.context(SerializeOptionsSnafu)?;
117117

118118
// Install the Helm chart using the Helm wrapper
119-
helm::install_release_from_repo_or_registry(
119+
helm::upgrade_or_install_release_from_repo_or_registry(
120120
&helm_chart.release_name,
121121
helm::ChartVersion {
122122
chart_source: &helm_chart.repo.name,

rust/stackable-cockpit/src/utils/k8s/client.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use snafu::{OptionExt, ResultExt, Snafu};
1515
use stackable_operator::{commons::listener::Listener, kvp::Labels};
1616
use tokio::sync::RwLock;
1717
use tracing::{Span, info, instrument};
18-
use tracing_indicatif::span_ext::IndicatifSpanExt as _;
18+
use tracing_indicatif::{indicatif_eprintln, span_ext::IndicatifSpanExt as _};
1919

2020
#[cfg(doc)]
2121
use crate::utils::k8s::ListParamsExt;
@@ -144,13 +144,48 @@ impl Client {
144144
}
145145
};
146146

147-
api.patch(
148-
&object.name_any(),
149-
&PatchParams::apply("stackablectl"),
150-
&Patch::Apply(object),
151-
)
152-
.await
153-
.context(KubeClientPatchSnafu)?;
147+
if let Some(existing_object) = api
148+
.get_opt(&object.name_any())
149+
.await
150+
.context(KubeClientFetchSnafu)?
151+
{
152+
object.metadata.resource_version = existing_object.resource_version();
153+
154+
api
155+
.patch(
156+
&object.name_any(),
157+
&PatchParams::apply("stackablectl"),
158+
&Patch::Merge(object.clone()),
159+
)
160+
.await
161+
.or_else(|e| {
162+
// If re-applying a Job fails due to immutability, print out the failed manifests instead of erroring out,
163+
// so the user can decide if the existing Job needs a deletion and recreation
164+
match (resource.kind.as_ref(), e) {
165+
// Errors for immutability in Kubernetes do not return meaningful `code`, `status`, or `reason` to filter on
166+
// Currently we have to check the `message` for the actual error we are looking for
167+
("Job", kube::Error::Api(e)) if e.message.contains("field is immutable") => {
168+
indicatif_eprintln!(
169+
"Deploying {kind}/{object_name} manifest failed due to immutability",
170+
kind = resource.kind,
171+
object_name = object.name_any().clone()
172+
);
173+
Ok(object)
174+
}
175+
(_, e) => {
176+
Err(e).context(KubeClientPatchSnafu)
177+
}
178+
}
179+
})?;
180+
} else {
181+
api.patch(
182+
&object.name_any(),
183+
&PatchParams::apply("stackablectl"),
184+
&Patch::Apply(object.clone()),
185+
)
186+
.await
187+
.context(KubeClientPatchSnafu)?;
188+
}
154189
}
155190

156191
Ok(())

rust/stackablectl/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ All notable changes to this project will be documented in this file.
77
### Fixed
88

99
- nix: Update nixpkgs and upgrade nodejs-18 to nodejs_20 ([#384]).
10+
- Switch to idempotent Helm installations for demos and stacks ([#386]).
11+
- Ignore failed re-application of Jobs due to immutability in demo and stack installations.
12+
Display those manifests to the user, so they can decide if they need to delete and recreate it ([#386]).
1013

1114
[#384]: https://github.com/stackabletech/stackable-cockpit/pull/384
15+
[#386]: https://github.com/stackabletech/stackable-cockpit/pull/386
1216

1317
## [1.0.0] - 2025-06-02
1418

0 commit comments

Comments
 (0)